// Email + password login with signed session cookies. No external auth dependency. // Logins live in data/dashboard/users.json (see users.js); admins manage them in Settings. // Each request re-checks the user, so removing, deactivating or resetting someone signs them out at once. import crypto from "node:crypto"; import { SESSION_SECRET, SESSION_HOURS, COOKIE_SECURE } from "./config.js"; import * as users from "./users.js"; const COOKIE = "mc_session"; function sign(payload) { const body = Buffer.from(JSON.stringify(payload)).toString("base64url"); const sig = crypto.createHmac("sha256", SESSION_SECRET).update(body).digest("base64url"); return `${body}.${sig}`; } function unsign(token) { const [body, sig] = String(token || "").split("."); if (!body || !sig) return null; const expected = crypto.createHmac("sha256", SESSION_SECRET).update(body).digest("base64url"); if (sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null; const payload = JSON.parse(Buffer.from(body, "base64url").toString()); return payload.exp > Date.now() ? payload : null; } function readCookie(req, name) { const raw = req.headers.cookie || ""; for (const part of raw.split(";")) { const [k, ...v] = part.trim().split("="); if (k === name) return decodeURIComponent(v.join("=")); } return null; } function setCookie(res, value, maxAgeSec) { const parts = [`${COOKIE}=${encodeURIComponent(value)}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${maxAgeSec}`]; if (COOKIE_SECURE) parts.push("Secure"); res.setHeader("Set-Cookie", parts.join("; ")); } /** Start (or refresh, e.g. after a password change) the session for a user {id, v}. */ export function issueSession(res, user) { setCookie(res, sign({ id: user.id, v: user.v, exp: Date.now() + SESSION_HOURS * 3600 * 1000 }), SESSION_HOURS * 3600); } // Simple brute-force protection: 8 failed attempts per IP per 15 minutes. const failures = new Map(); function tooManyAttempts(ip) { const now = Date.now(); const list = (failures.get(ip) || []).filter((t) => now - t < 15 * 60 * 1000); failures.set(ip, list); return list.length >= 8; } export function login(req, res) { const ip = req.ip || "unknown"; if (tooManyAttempts(ip)) return res.status(429).json({ error: "Too many attempts. Try again in 15 minutes." }); const { email, password } = req.body || {}; const user = users.authenticate(email, password); if (!user) { failures.get(ip).push(Date.now()); return res.status(401).json({ error: "Wrong email or password." }); } issueSession(res, user); const { v, ...pub } = user; res.json({ user: pub }); } /** First run only: create the first admin from the sign-in screen when no logins exist. */ export function setupFirstAdmin(req, res) { if (users.hasUsers()) return res.status(403).json({ error: "Setup is already done. Sign in instead." }); const { email, name, password } = req.body || {}; try { if (!password) throw Object.assign(new Error("Choose a password."), { status: 400 }); const { user } = users.createUser({ email, name, password, role: "admin" }, "first-run setup"); issueSession(res, { id: user.id, v: 1 }); res.json({ user }); } catch (e) { res.status(e.status || 400).json({ error: e.message }); } } export function logout(_req, res) { setCookie(res, "", 0); res.json({ ok: true }); } export function currentUser(req) { const s = unsign(readCookie(req, COOKIE)); return s?.id ? users.sessionUser(s.id, s.v) : null; } export function requireAuth(req, res, next) { const user = currentUser(req); if (!user) return res.status(401).json({ error: "Not signed in." }); req.user = user; next(); } export function requireAdmin(req, res, next) { if (req.user?.role !== "admin") return res.status(403).json({ error: "Admins only." }); next(); }