Files
freno-dev/src/server/session-management.ts
Michael Freno f056cf05d3 hopeful
2026-01-07 20:19:26 -05:00

196 lines
5.0 KiB
TypeScript

import { ConnectionFactory } from "./database";
import type { Session } from "~/db/types";
import { formatDeviceDescription } from "./device-utils";
/**
* Get all active sessions for a user
* @param userId - User ID
* @returns Array of active sessions with formatted device info
*/
export async function getUserActiveSessions(userId: string): Promise<
Array<{
sessionId: string;
deviceDescription: string;
deviceType?: string;
browser?: string;
os?: string;
ipAddress?: string;
lastActive: string;
createdAt: string;
current: boolean;
}>
> {
const conn = ConnectionFactory();
const result = await conn.execute({
sql: `SELECT
id, device_name, device_type, browser, os,
ip_address, last_active_at, created_at, token_family
FROM Session
WHERE user_id = ? AND revoked = 0 AND expires_at > datetime('now')
ORDER BY last_active_at DESC`,
args: [userId]
});
return result.rows.map((row: any) => {
const deviceInfo = {
deviceName: row.device_name,
deviceType: row.device_type,
browser: row.browser,
os: row.os
};
return {
sessionId: row.id,
deviceDescription: formatDeviceDescription(deviceInfo),
deviceType: row.device_type,
browser: row.browser,
os: row.os,
ipAddress: row.ip_address,
lastActive: row.last_active_at,
createdAt: row.created_at,
current: false // Will be set by caller if needed
};
});
}
/**
* Revoke a specific session (not entire token family)
* Useful for "logout from this device" functionality
* @param userId - User ID (for verification)
* @param sessionId - Session ID to revoke
* @throws Error if session not found or doesn't belong to user
*/
export async function revokeUserSession(
userId: string,
sessionId: string
): Promise<void> {
const conn = ConnectionFactory();
// Verify session belongs to user
const verifyResult = await conn.execute({
sql: "SELECT user_id FROM Session WHERE id = ?",
args: [sessionId]
});
if (verifyResult.rows.length === 0) {
throw new Error("Session not found");
}
const sessionUserId = (verifyResult.rows[0] as any).user_id;
if (sessionUserId !== userId) {
throw new Error("Session does not belong to this user");
}
// Revoke the session
await conn.execute({
sql: "UPDATE Session SET revoked = 1 WHERE id = ?",
args: [sessionId]
});
}
/**
* Revoke all sessions for a user EXCEPT the current one
* Useful for "logout from all other devices"
* @param userId - User ID
* @param currentSessionId - Current session ID to keep active
* @returns Number of sessions revoked
*/
export async function revokeOtherUserSessions(
userId: string,
currentSessionId: string
): Promise<number> {
const conn = ConnectionFactory();
const result = await conn.execute({
sql: "UPDATE Session SET revoked = 1 WHERE user_id = ? AND id != ? AND revoked = 0",
args: [userId, currentSessionId]
});
return (result as any).rowsAffected || 0;
}
/**
* Get session count by device type for a user
* @param userId - User ID
* @returns Object with counts by device type
*/
export async function getSessionCountByDevice(userId: string): Promise<{
desktop: number;
mobile: number;
tablet: number;
unknown: number;
total: number;
}> {
const conn = ConnectionFactory();
const result = await conn.execute({
sql: `SELECT
device_type,
COUNT(*) as count
FROM Session
WHERE user_id = ? AND revoked = 0 AND expires_at > datetime('now')
GROUP BY device_type`,
args: [userId]
});
const counts = {
desktop: 0,
mobile: 0,
tablet: 0,
unknown: 0,
total: 0
};
for (const row of result.rows) {
const deviceType = (row as any).device_type;
const count = (row as any).count;
if (deviceType === "desktop") {
counts.desktop = count;
} else if (deviceType === "mobile") {
counts.mobile = count;
} else if (deviceType === "tablet") {
counts.tablet = count;
} else {
counts.unknown = count;
}
counts.total += count;
}
return counts;
}
/**
* Check if a specific device fingerprint already has an active session
* Can be used to show "You're already logged in on this device" messages
* @param userId - User ID
* @param deviceType - Device type
* @param browser - Browser name
* @param os - OS name
* @returns true if device has active session
*/
export async function hasActiveSessionOnDevice(
userId: string,
deviceType?: string,
browser?: string,
os?: string
): Promise<boolean> {
const conn = ConnectionFactory();
const result = await conn.execute({
sql: `SELECT id FROM Session
WHERE user_id = ?
AND device_type = ?
AND browser = ?
AND os = ?
AND revoked = 0
AND expires_at > datetime('now')
LIMIT 1`,
args: [userId, deviceType || null, browser || null, os || null]
});
return result.rows.length > 0;
}