Requests / src /lib /adminAuth.ts
armand0e's picture
init
101ebaa
import crypto from "crypto";
import type { NextRequest } from "next/server";
export const ADMIN_COOKIE_NAME = "teich_admin";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
function getAdminPassword() {
return process.env.ADMIN_PASSWORD || "";
}
function sign(payload: string, secret: string) {
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
}
export function createAdminSessionValue(): string {
const secret = getAdminPassword();
const ts = Date.now();
const nonce = crypto.randomBytes(16).toString("hex");
const payload = `${ts}:${nonce}`;
const sig = sign(payload, secret);
return `${payload}.${sig}`;
}
export function isAdminSessionValue(value: string | undefined | null): boolean {
if (!value) return false;
const secret = getAdminPassword();
if (!secret) return false;
const lastDot = value.lastIndexOf(".");
if (lastDot === -1) return false;
const payload = value.slice(0, lastDot);
const sig = value.slice(lastDot + 1);
const expected = sign(payload, secret);
try {
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return false;
} catch {
return false;
}
const [tsStr] = payload.split(":");
const ts = Number(tsStr);
if (!Number.isFinite(ts)) return false;
const ageSeconds = (Date.now() - ts) / 1000;
if (ageSeconds < 0 || ageSeconds > SESSION_MAX_AGE_SECONDS) return false;
return true;
}
export function isAdminRequest(request: NextRequest): boolean {
const value = request.cookies.get(ADMIN_COOKIE_NAME)?.value;
return isAdminSessionValue(value);
}
export function adminCookieOptions() {
return {
name: ADMIN_COOKIE_NAME,
httpOnly: true,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: SESSION_MAX_AGE_SECONDS,
};
}