| 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, | |
| }; | |
| } | |