import { randomBytes, createHash } from "node:crypto"; import { cookies } from "next/headers"; import { prisma } from "@/lib/prisma"; import { env } from "@/lib/env"; export const SESSION_COOKIE = "mrp_session"; export type Role = "admin" | "operator"; export interface SessionUser { id: string; role: Role; name: string; email: string | null; } function sha256(value: string): string { return createHash("sha256").update(value).digest("hex"); } function newToken(): { token: string; hash: string } { const token = randomBytes(32).toString("base64url"); return { token, hash: sha256(token) }; } function ttlForRole(role: Role): number { const hours = role === "admin" ? env.ADMIN_SESSION_HOURS : env.OPERATOR_SESSION_HOURS; return hours * 60 * 60 * 1000; } export interface CreateSessionInput { userId: string; role: Role; userAgent?: string | null; ipAddress?: string | null; deviceLabel?: string | null; } export async function createSession(input: CreateSessionInput): Promise<{ token: string; expiresAt: Date }> { const { token, hash } = newToken(); const expiresAt = new Date(Date.now() + ttlForRole(input.role)); await prisma.session.create({ data: { userId: input.userId, tokenHash: hash, expiresAt, userAgent: input.userAgent ?? null, ipAddress: input.ipAddress ?? null, deviceLabel: input.deviceLabel ?? null, }, }); const jar = await cookies(); jar.set(SESSION_COOKIE, token, { httpOnly: true, sameSite: "lax", secure: env.NODE_ENV === "production", path: "/", expires: expiresAt, }); return { token, expiresAt }; } export async function getSessionUser(): Promise { const jar = await cookies(); const token = jar.get(SESSION_COOKIE)?.value; if (!token) return null; const hash = sha256(token); const session = await prisma.session.findUnique({ where: { tokenHash: hash }, include: { user: true }, }); if (!session) return null; if (session.expiresAt.getTime() < Date.now()) { await prisma.session.delete({ where: { id: session.id } }).catch(() => {}); return null; } if (!session.user.active) return null; // touch lastSeenAt at most once per minute to limit write load const stale = Date.now() - session.lastSeenAt.getTime() > 60_000; if (stale) { prisma.session .update({ where: { id: session.id }, data: { lastSeenAt: new Date() } }) .catch(() => {}); } return { id: session.user.id, role: session.user.role as Role, name: session.user.name, email: session.user.email, }; } export async function destroyCurrentSession(): Promise { const jar = await cookies(); const token = jar.get(SESSION_COOKIE)?.value; if (token) { await prisma.session.deleteMany({ where: { tokenHash: sha256(token) } }); } jar.delete(SESSION_COOKIE); } export async function purgeExpiredSessions(): Promise { const result = await prisma.session.deleteMany({ where: { expiresAt: { lt: new Date() } }, }); return result.count; }