feat(web): add iron-session wrapper (web/lib/auth.ts)

This commit is contained in:
yiekheng 2026-05-03 08:27:05 +08:00
parent f2facb200f
commit a8751b6731

61
web/lib/auth.ts Normal file
View File

@ -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<Session | null> {
const jar = await cookies();
const raw = jar.get(COOKIE_NAME)?.value;
if (!raw) return null;
try {
return await unsealData<Session>(raw, { password: secret() });
} catch {
return null;
}
}
export async function setSession(session: Session): Promise<void> {
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<void> {
const jar = await cookies();
jar.delete(COOKIE_NAME);
}
export async function requireSession(): Promise<Session> {
const s = await getSession();
if (!s) throw new Error("Unauthenticated");
return s;
}