| import type { Request, Response } from 'express' |
| import type { DbSchema, DbSession, SessionKind } from './db.js' |
| import { newId, nowIso, pruneExpiredSessions } from './db.js' |
|
|
| export const COOKIE_FULL = 'sid' |
| export const COOKIE_PENDING = 'psid' |
|
|
| function addMinutes(date: Date, minutes: number) { |
| return new Date(date.getTime() + minutes * 60_000) |
| } |
|
|
| function addDays(date: Date, days: number) { |
| return new Date(date.getTime() + days * 86_400_000) |
| } |
|
|
| export function createSession(db: DbSchema, userId: string, kind: SessionKind): DbSession { |
| pruneExpiredSessions(db) |
| const now = new Date() |
| const expires = kind === 'mfa_pending' ? addMinutes(now, 10) : addDays(now, 7) |
|
|
| const s: DbSession = { |
| id: newId(), |
| user_id: userId, |
| kind, |
| created_at: nowIso(), |
| expires_at: expires.toISOString(), |
| } |
| db.sessions.push(s) |
| return s |
| } |
|
|
| function isSecureRequest(req: Request) { |
| if (req.secure) return true |
| const xfp = req.headers['x-forwarded-proto'] |
| if (typeof xfp === 'string') return xfp.split(',')[0]?.trim() === 'https' |
| return false |
| } |
|
|
| function cookieBaseOptions(req: Request) { |
| return { |
| httpOnly: true, |
| sameSite: 'lax' as const, |
| secure: isSecureRequest(req), |
| } |
| } |
|
|
| export function clearSessionCookies(req: Request, res: Response) { |
| const base = cookieBaseOptions(req) |
| res.clearCookie(COOKIE_FULL, base) |
| res.clearCookie(COOKIE_PENDING, base) |
| } |
|
|
| export function setSessionCookie(req: Request, res: Response, session: DbSession) { |
| const name = session.kind === 'mfa_pending' ? COOKIE_PENDING : COOKIE_FULL |
| const base = cookieBaseOptions(req) |
| res.cookie(name, session.id, { |
| ...base, |
| expires: new Date(session.expires_at), |
| }) |
| } |
|
|
| export function getSessionFromRequest(req: Request, kind: SessionKind) { |
| const cookies = (req as unknown as { cookies?: Record<string, unknown> }).cookies |
| const raw = cookies?.[kind === 'mfa_pending' ? COOKIE_PENDING : COOKIE_FULL] |
| return typeof raw === 'string' ? raw : null |
| } |
|
|
| export function removeSessionsByIds(db: DbSchema, ids: string[]) { |
| if (ids.length === 0) return |
| const set = new Set(ids) |
| db.sessions = db.sessions.filter((s) => !set.has(s.id)) |
| } |
|
|
| export function removeOtherUserSessions(db: DbSchema, userId: string, keepSessionIds: string[]) { |
| const keep = new Set(keepSessionIds) |
| db.sessions = db.sessions.filter((s) => s.user_id !== userId || keep.has(s.id)) |
| } |
|
|