/** * Auth Route — POST /api/auth/login, GET /api/auth/verify * * Password stored in DASHBOARD_PASSWORD env var (never in code). * Token: HMAC-SHA256(expiresAt, password) — signed, time-limited, stateless. */ import { Router, Request, Response } from "express"; import { createHmac, timingSafeEqual } from "crypto"; export const authRouter = Router(); const TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days function sign(expiresAt: number): string { const password = process.env.DASHBOARD_PASSWORD ?? ""; return createHmac("sha256", password) .update(String(expiresAt)) .digest("base64url"); } export function verifyToken(token: string): boolean { if (!token || !token.includes(".")) return false; const [rawExp, sig] = token.split(".", 2); const expiresAt = parseInt(rawExp, 10); if (isNaN(expiresAt) || Date.now() > expiresAt) return false; const expected = sign(expiresAt); try { return timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } catch { return false; } } // POST /api/auth/login authRouter.post("/login", (req: Request, res: Response) => { const { password } = req.body as { password?: string }; const envPassword = process.env.DASHBOARD_PASSWORD; if (!envPassword) { res.status(500).json({ error: "Auth not configured (DASHBOARD_PASSWORD missing)" }); return; } if (!password) { res.status(400).json({ error: "Password required" }); return; } // Constant-time comparison to prevent timing attacks const pwdBuf = Buffer.from(password.padEnd(128, "\0").slice(0, 128)); const envBuf = Buffer.from(envPassword.padEnd(128, "\0").slice(0, 128)); const match = timingSafeEqual(pwdBuf, envBuf) && password === envPassword; if (!match) { res.status(401).json({ error: "Invalid password" }); return; } const expiresAt = Date.now() + TOKEN_TTL_MS; const token = `${expiresAt}.${sign(expiresAt)}`; res.json({ token, expiresAt }); }); // GET /api/auth/verify authRouter.get("/verify", (req: Request, res: Response) => { const auth = req.headers.authorization ?? ""; const token = auth.startsWith("Bearer ") ? auth.slice(7) : ""; if (verifyToken(token)) { res.json({ ok: true }); } else { res.status(401).json({ ok: false, error: "Unauthorized" }); } });