fifteen/apps/web/lib/admin-auth.ts
2026-03-01 21:59:44 +01:00

72 lines
2 KiB
TypeScript

import { SignJWT, jwtVerify, type JWTPayload } from "jose";
import type { NextRequest } from "next/server";
const SESSION_COOKIE = "admin_session";
const SESSION_TTL = 28_800; // 8 hours in seconds
function getJwtSecret(): Uint8Array {
const secret = process.env.ADMIN_JWT_SECRET;
if (!secret || secret.length < 32) {
throw new Error(
"ADMIN_JWT_SECRET env var must be set to a string of at least 32 characters",
);
}
return new TextEncoder().encode(secret);
}
export interface AdminSession extends JWTPayload {
ip: string;
}
/** Creates a signed JWT and returns it as the cookie value. */
export async function createSession(ip: string): Promise<string> {
const token = await new SignJWT({ ip } satisfies Partial<AdminSession>)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(`${SESSION_TTL}s`)
.sign(getJwtSecret());
return token;
}
/**
* Verifies the session JWT from the request cookie.
* Works in both Edge and Node.js runtimes (uses Web Crypto).
*/
export async function getSession(
req: NextRequest,
): Promise<AdminSession | null> {
const token = req.cookies.get(SESSION_COOKIE)?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify<AdminSession>(token, getJwtSecret());
return payload;
} catch {
return null;
}
}
/**
* Destroys a session — with JWT sessions the cookie is cleared client-side.
* No server-side state to remove.
*/
export async function destroySession(_req: NextRequest): Promise<void> {
// JWT sessions are stateless — clearing the cookie in the response is sufficient.
}
export const SESSION_COOKIE_NAME = SESSION_COOKIE;
export function makeSessionCookie(token: string, secure: boolean): string {
const parts = [
`${SESSION_COOKIE}=${token}`,
"HttpOnly",
"SameSite=Strict",
`Max-Age=${SESSION_TTL}`,
"Path=/",
];
if (secure) parts.push("Secure");
return parts.join("; ");
}
export function clearSessionCookie(): string {
return `${SESSION_COOKIE}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/`;
}