GuDingZiChan/backend/src/utils/sessionStore.ts

123 lines
3.5 KiB
TypeScript

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<SessionData> | 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()
}