hopeful
This commit is contained in:
195
src/server/session-management.ts
Normal file
195
src/server/session-management.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user