diff --git a/web/lib/auth.ts b/web/lib/auth.ts new file mode 100644 index 0000000..b8862b8 --- /dev/null +++ b/web/lib/auth.ts @@ -0,0 +1,61 @@ +import "server-only"; +import { cookies } from "next/headers"; +import { sealData, unsealData } from "iron-session"; + +const COOKIE_NAME = "cm_auth"; +const COOKIE_TTL_SECONDS = 30 * 24 * 60 * 60; + +export type Session = { + username: string; + authenticatedAt: number; + pendingChallenge?: { + kind: "register" | "authenticate"; + challenge: string; + expiresAt: number; + }; +}; + +function secret(): string { + const s = process.env.CM_AUTH_SECRET; + if (!s || s.length < 32) { + throw new Error("CM_AUTH_SECRET missing or shorter than 32 chars"); + } + return s; +} + +export async function getSession(): Promise { + const jar = await cookies(); + const raw = jar.get(COOKIE_NAME)?.value; + if (!raw) return null; + try { + return await unsealData(raw, { password: secret() }); + } catch { + return null; + } +} + +export async function setSession(session: Session): Promise { + const sealed = await sealData(session, { + password: secret(), + ttl: COOKIE_TTL_SECONDS, + }); + const jar = await cookies(); + jar.set(COOKIE_NAME, sealed, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: COOKIE_TTL_SECONDS, + }); +} + +export async function clearSession(): Promise { + const jar = await cookies(); + jar.delete(COOKIE_NAME); +} + +export async function requireSession(): Promise { + const s = await getSession(); + if (!s) throw new Error("Unauthenticated"); + return s; +}