56 lines
2.1 KiB
TypeScript
56 lines
2.1 KiB
TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
import { cookies } from "next/headers";
|
|
import { isSecureDeployment } from "@/lib/security/cookie-security";
|
|
import { getManagerConfig } from "./manager-config";
|
|
|
|
const COOKIE_NAME = "nextchair_manager_session";
|
|
|
|
function signature(value: string, secret: string) {
|
|
return createHmac("sha256", secret).update(value).digest("hex");
|
|
}
|
|
|
|
export function isManagerConfigured() {
|
|
const config = getManagerConfig();
|
|
return Boolean(config.adminEmail && config.adminPassword && config.authSecret);
|
|
}
|
|
|
|
export function authenticateManager(email: string, password: string) {
|
|
const config = getManagerConfig();
|
|
if (!isManagerConfigured() || email.trim().toLowerCase() !== config.adminEmail.trim().toLowerCase()) return false;
|
|
const expected = Buffer.from(config.adminPassword);
|
|
const received = Buffer.from(password);
|
|
return expected.length === received.length && timingSafeEqual(expected, received);
|
|
}
|
|
|
|
export async function createManagerSession() {
|
|
const config = getManagerConfig();
|
|
const value = `${config.adminEmail.trim().toLowerCase()}.${Date.now()}`;
|
|
(await cookies()).set(COOKIE_NAME, `${value}.${signature(value, config.authSecret)}`, {
|
|
httpOnly: true,
|
|
sameSite: "strict",
|
|
secure: isSecureDeployment(),
|
|
// The Manager UI is under /manager, but its authenticated API is under
|
|
// /api/manager, so the cookie must cover both paths.
|
|
path: "/",
|
|
maxAge: 60 * 60 * 8,
|
|
});
|
|
}
|
|
|
|
export async function clearManagerSession() {
|
|
(await cookies()).delete(COOKIE_NAME);
|
|
}
|
|
|
|
export async function isManagerAuthenticated() {
|
|
const config = getManagerConfig();
|
|
if (!config.authSecret) return false;
|
|
const rawValue = (await cookies()).get(COOKIE_NAME)?.value;
|
|
const value = rawValue ? decodeURIComponent(rawValue) : undefined;
|
|
if (!value) return false;
|
|
const separator = value.lastIndexOf(".");
|
|
if (separator < 1) return false;
|
|
const payload = value.slice(0, separator);
|
|
const received = Buffer.from(value.slice(separator + 1));
|
|
const expected = Buffer.from(signature(payload, config.authSecret));
|
|
return received.length === expected.length && timingSafeEqual(received, expected);
|
|
}
|