import session, { type SessionData } from 'express-session' import prisma from './prisma' const DEFAULT_SESSION_MAX_AGE_MS = 60 * 60 * 1000 const CLEANUP_INTERVAL_MS = 15 * 60 * 1000 type StoreCallback = (error?: unknown) => void type StoreGetCallback = (error: unknown, session?: SessionData | null) => void const finish = (callback: StoreCallback | undefined, error?: unknown) => { if (callback) callback(error) } export const resolveSessionExpiresAt = ( sessionData: Partial | null | undefined, now: Date = new Date() ) => { const cookie = sessionData?.cookie as SessionData['cookie'] | undefined const explicitExpires = cookie?.expires ? new Date(cookie.expires) : null if (explicitExpires && !Number.isNaN(explicitExpires.getTime())) { return explicitExpires } const maxAge = Number(cookie?.maxAge) return new Date( now.getTime() + (Number.isFinite(maxAge) && maxAge > 0 ? maxAge : DEFAULT_SESSION_MAX_AGE_MS) ) } export const serializeSession = (sessionData: SessionData) => JSON.stringify(sessionData) export const deserializeSession = (value: string): SessionData => { const parsed = JSON.parse(value) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('持久化会话内容无效') } return parsed as SessionData } export class PrismaSessionStore extends session.Store { get(sid: string, callback: StoreGetCallback) { void (async () => { try { const stored = await prisma.webSession.findUnique({ where: { sid }, select: { data: true, expires_at: true } }) if (!stored) { callback(null, null) return } if (stored.expires_at.getTime() <= Date.now()) { await prisma.webSession.deleteMany({ where: { sid } }) callback(null, null) return } callback(null, deserializeSession(stored.data)) } catch (error) { callback(error) } })() } set(sid: string, sessionData: SessionData, callback?: StoreCallback) { void (async () => { try { const data = serializeSession(sessionData) const expiresAt = resolveSessionExpiresAt(sessionData) await prisma.webSession.upsert({ where: { sid }, create: { sid, data, expires_at: expiresAt }, update: { data, expires_at: expiresAt } }) finish(callback) } catch (error) { finish(callback, error) } })() } destroy(sid: string, callback?: StoreCallback) { void (async () => { try { await prisma.webSession.deleteMany({ where: { sid } }) finish(callback) } catch (error) { finish(callback, error) } })() } touch(sid: string, sessionData: SessionData, callback?: StoreCallback) { void (async () => { try { await prisma.webSession.updateMany({ where: { sid, expires_at: { gt: new Date() } }, data: { expires_at: resolveSessionExpiresAt(sessionData) } }) finish(callback) } catch (error) { finish(callback, error) } })() } } export const persistentSessionStore = new PrismaSessionStore() export const startSessionCleanupWorker = () => { const cleanup = async () => { try { await prisma.webSession.deleteMany({ where: { expires_at: { lte: new Date() } } }) } catch (error) { console.error('[session] 清理过期会话失败:', error) } } setTimeout(cleanup, 30_000).unref() setInterval(cleanup, CLEANUP_INTERVAL_MS).unref() }