metatroncubeswdev c309581df0 Initial commit: Quorla social media manager
Dashboard (content library, publishing queue with approvals, inbox, team
management, AI runs), Meta connector, branded PDF reports, agent skills,
and Docker packaging (Dockerfile, docker-compose with Caddy HTTPS,
backup and path-relocation scripts).

Client data, live data and secrets are excluded via .gitignore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:02:29 -04:00

103 lines
3.8 KiB
JavaScript

// 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();
}