Management UI
CI / ci (push) Canceled after 0s

Attempt 2 of a management ui.
This commit is contained in:
Stefan Lazic committed 2026-09-05 13:39:55 +01:00
1 parent 9fe5b4f038
commit a0d39026c0
54 files changed
+8804 -267

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Signed-cookie session for the manager app — adapted from (not imported
* from) the main NextChair app's src/lib/booking-draft/sign.ts and
* src/lib/security/secrets-match.ts. Deliberately copied rather than
* shared: this app must never import anything from the main app's src/ —
* a previous attempt at this exact tool was built as routes inside the
* main app and repeatedly broke from transitively importing Prisma/
* Auth.js/business data into a mode that has no business database at
* all — see ../../README.md's "Why it's a separate app" for the full
* history. Keeping this app's module graph fully separate is what makes
* that failure structurally impossible to repeat here.
*
* HMAC-signed (tamper-evident), not encrypted — the payload is just an
* email and an expiry, nothing sensitive to disclose to the browser
* holding its own cookie. Session length: 7 days.
*/
const CONTEXT = "nextchair-manager-session-v1";
const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
export interface ManagerSession {
email: string;
exp: number;
}
function getSigningKey(): Buffer {
const secret = process.env.MANAGER_SESSION_SECRET;
if (!secret) {
throw new Error("MANAGER_SESSION_SECRET must be set.");
}
return createHmac("sha256", secret).update(CONTEXT).digest();
}
function sign(payload: string): string {
const mac = createHmac("sha256", getSigningKey()).update(payload).digest("base64url");
const encodedPayload = Buffer.from(payload, "utf8").toString("base64url");
return `${encodedPayload}.${mac}`;
}
function verifyToken(token: string): string | null {
const separatorIndex = token.indexOf(".");
if (separatorIndex === -1) return null;
const encodedPayload = token.slice(0, separatorIndex);
const mac = token.slice(separatorIndex + 1);
if (!encodedPayload || !mac) return null;
let payload: string;
try {
payload = Buffer.from(encodedPayload, "base64url").toString("utf8");
} catch {
return null;
}
const expectedMac = createHmac("sha256", getSigningKey()).update(payload).digest("base64url");
const macBuffer = Buffer.from(mac);
const expectedBuffer = Buffer.from(expectedMac);
if (macBuffer.length !== expectedBuffer.length) return null;
if (!timingSafeEqual(macBuffer, expectedBuffer)) return null;
return payload;
}
export function createSessionToken(email: string): string {
const payload: ManagerSession = { email, exp: Date.now() + SESSION_TTL_MS };
return sign(JSON.stringify(payload));
}
/** Returns the session if the token is validly signed and not expired, otherwise null. Never throws on malformed input. */
export function verifySessionToken(token: string | undefined): ManagerSession | null {
if (!token) return null;
const payload = verifyToken(token);
if (!payload) return null;
let session: ManagerSession;
try {
session = JSON.parse(payload);
} catch {
return null;
}
if (typeof session.email !== "string" || typeof session.exp !== "number") return null;
if (Date.now() > session.exp) return null;
return session;
}
/**
* Same signal/reasoning as the main app's src/lib/security/cookie-
* security.ts's isSecureDeployment() — NODE_ENV=production doesn't mean
* TLS is actually in front of this app, so the cookie's Secure attribute
* is derived from the deployer-declared public URL instead.
*/
export function isSecureDeployment(): boolean {
return (process.env.MANAGER_PUBLIC_URL ?? "").startsWith("https://");
}
export const SESSION_COOKIE_NAME = "manager_session";