import Crypto from "node:crypto"; import type { User } from "$lib/server/database"; import Logs from "$lib/server/log" interface UserSession { user: User; last_active: Date; } export interface TokenInfo { user_id: number; creation_time: Date; expiry_time: Date; } class LoginError extends Error { constructor(message: string) { super(message); } } class UserLoginError extends LoginError { user_id: number; constructor(user_id: number, message: string) { super(message); this.user_id = user_id; } } const TOKEN_LENGTH = 256; const active_users = new Map(); const active_session_tokens = new Map(); function issue_access_token_for_user(user: User, expiry_date: Date): string { Logs.user.info(`Issuing access token for user id ${user.id}. Expiry date: ${expiry_date.toISOString()}`) let user_session = active_users.get(user.id); if (user_session === undefined) { user_session = { user: user, last_active: new Date() } as UserSession; active_users.set(user.id, user_session); } /* | Token (256) | Expiry date as getTime | */ const token_info: TokenInfo = { user_id: user.id, creation_time: new Date(), expiry_time: expiry_date } const session_token = Buffer.concat( [ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3), Buffer.from(token_info.creation_time.getTime().toFixed(0)) ] ).toString("base64"); if (active_session_tokens.has(session_token)) { throw new UserLoginError(user.id, "Tried issuing a duplicate session token."); } active_session_tokens.set(session_token, token_info); return session_token; } function get_user_by_access_token(token: string): User | null { const token_info = active_session_tokens.get(token); if (token_info === undefined || token_info.expiry_time.getTime() < Date.now()) { return null; } const user = active_users.get(token_info.user_id); if (user === undefined) { return null; } user.last_active = new Date(); return user.user; } function logout_user_session(token: string): boolean { const token_info = active_session_tokens.get(token); if (!token_info) { Logs.user.warn(`Failed to logout user by token, because token does not exist`); return false; } token_info.expiry_time = new Date(0); return true; } async function __clean_session_store() { let cleaned_user_sessions = 0 let cleaned_active_users = 0 const active_users_session = new Set(); active_session_tokens.forEach((token_info, token, token_map) => { if (token_info.expiry_time.getTime() < Date.now()) { token_map.delete(token); cleaned_user_sessions += 1 } else { active_users_session.add(token_info.user_id); } }); active_users.forEach((user_info, user_id, user_map) => { if (user_info.last_active.getTime() + 15*60*1000 < Date.now() && !active_users.has(user_id)) { user_map.delete(user_id) cleaned_active_users += 1 } }); Logs.user.info(`Cleaned ${cleaned_user_sessions} inactive session tokens and ${cleaned_active_users} inactive users`) } export default class SessionStore { static issue_access_token_for_user = issue_access_token_for_user; static get_user_by_access_token = get_user_by_access_token; static logout_user_session = logout_user_session; } setInterval(__clean_session_store, 15*60*1000);