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 : "/";
}