123 lines
3.5 KiB
TypeScript
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()
|
|
}
|