import "server-only"; import { createHash, randomBytes } from "node:crypto"; import { cache } from "react"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { db } from "./db"; import { getUserById } from "./users"; const SESSION_COOKIE = "session"; const SESSION_DAYS = 30; const sha256 = (s: string) => createHash("sha256").update(s).digest("hex"); export async function startSession(userId: number) { const token = randomBytes(32).toString("base64url"); const now = Date.now(); const expires = now + SESSION_DAYS * 86400e3; db.prepare("INSERT INTO sessions (id, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)").run( sha256(token), userId, now, expires, ); const h = await headers(); (await cookies()).set(SESSION_COOKIE, token, { httpOnly: true, sameSite: "lax", secure: h.get("x-forwarded-proto") === "https", path: "/", expires: new Date(expires), }); } export async function endSession() { const jar = await cookies(); const token = jar.get(SESSION_COOKIE)?.value; if (token) db.prepare("DELETE FROM sessions WHERE id = ?").run(sha256(token)); jar.delete(SESSION_COOKIE); } /** The signed-in user for this request, or null. */ export const getViewer = cache(async () => { const token = (await cookies()).get(SESSION_COOKIE)?.value; if (!token) return null; const session = db.prepare("SELECT user_id, expires_at FROM sessions WHERE id = ?").get(sha256(token)) as | { user_id: number; expires_at: number } | undefined; if (!session || session.expires_at < Date.now()) return null; return getUserById(session.user_id); }); /** Returns the signed-in user, or redirects to the login page. */ export async function requireViewer(returnTo?: string) { const viewer = await getViewer(); if (!viewer) redirect(returnTo ? `/login?next=${encodeURIComponent(returnTo)}` : "/login"); return viewer; } /** Only allow same-site relative redirects after login. */ export function safeNext(next: unknown) { return typeof next === "string" && next.startsWith("/") && !next.startsWith("//") ? next : "/"; }