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