dragonflight/services/mam-api/src/routes/auth.js

326 lines
13 KiB
JavaScript
Raw Normal View History

/**
* Authentication routes
*
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
* POST /api/v1/auth/login exchange username+password for a session
* POST /api/v1/auth/logout destroy the current session
* GET /api/v1/auth/me return the currently authenticated user
* GET /api/v1/auth/setup-status tell login.html whether to show setup
* POST /api/v1/auth/setup one-time first-admin bootstrap from UI
* POST /api/v1/auth/password change current user's password
* PATCH /api/v1/auth/me update current user's display_name
*
* Sessions are stored in PG via connect-pg-simple (see index.js). Bearer
* tokens go through middleware/auth.js, not here.
*/
import express from 'express';
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
import bcrypt from 'bcrypt';
import pool from '../db/pool.js';
import audit from '../middleware/audit.js';
import { checkPassword } from '../middleware/passwordPolicy.js';
const router = express.Router();
// ---------------------------------------------------------------------------
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// In-memory login rate limiter.
//
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// Tracks failed attempts per (IP, username). After MAX_ATTEMPTS failures
// within WINDOW_MS the endpoint returns 429 for LOCKOUT_MS regardless of
// the password supplied. Simple by design — no Redis dependency, single
// replica deploy. For multi-replica add an external store later.
// ---------------------------------------------------------------------------
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const MAX_ATTEMPTS = parseInt(process.env.LOGIN_MAX_ATTEMPTS || '10', 10);
const WINDOW_MS = parseInt(process.env.LOGIN_WINDOW_MS || String(15 * 60 * 1000), 10);
const LOCKOUT_MS = parseInt(process.env.LOGIN_LOCKOUT_MS || String(15 * 60 * 1000), 10);
const loginAttempts = new Map();
setInterval(() => {
const now = Date.now();
for (const [key, entry] of loginAttempts.entries()) {
const expired = entry.lockedUntil
? now > entry.lockedUntil
: now - entry.firstAttempt > WINDOW_MS;
if (expired) loginAttempts.delete(key);
}
}, 10 * 60 * 1000).unref();
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
function attemptKey(req, username) {
const ip = req.ip || req.socket?.remoteAddress || 'unknown';
return `${ip}:${String(username || '').trim().toLowerCase()}`;
}
function checkRateLimit(req, username) {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const key = attemptKey(req, username);
const entry = loginAttempts.get(key);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const now = Date.now();
if (!entry) return { limited: false };
if (entry.lockedUntil && now < entry.lockedUntil) {
return { limited: true, retryAfter: Math.ceil((entry.lockedUntil - now) / 1000) };
}
if (now - entry.firstAttempt > WINDOW_MS) {
loginAttempts.delete(key);
}
return { limited: false };
}
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
function recordFail(req, username) {
const key = attemptKey(req, username);
const now = Date.now();
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const entry = loginAttempts.get(key) || { attempts: 0, firstAttempt: now, lockedUntil: null };
entry.attempts += 1;
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
if (entry.attempts >= MAX_ATTEMPTS) entry.lockedUntil = now + LOCKOUT_MS;
loginAttempts.set(key, entry);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
return entry.attempts;
}
function clearAttempts(req, username) {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
loginAttempts.delete(attemptKey(req, username));
}
// ---------------------------------------------------------------------------
// POST /login
// ---------------------------------------------------------------------------
router.post('/login', async (req, res, next) => {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const rate = checkRateLimit(req, username);
if (rate.limited) {
res.set('Retry-After', String(rate.retryAfter));
audit(req, 'auth.lockout', { meta: { username, retry_after_sec: rate.retryAfter } });
return res.status(429).json({
error: `Too many failed attempts. Try again in ${rate.retryAfter} seconds.`,
});
}
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
try {
const result = await pool.query(
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
'SELECT id, username, password_hash, display_name, role, is_client FROM users WHERE username = $1',
[String(username).trim().toLowerCase()]
);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const user = result.rows[0];
// Constant-time path: even on missing user, run bcrypt against a dummy
// hash so attackers can't enumerate usernames by response time.
if (!user) {
await bcrypt.compare(password, '$2b$12$invalidhashpadding000000000000000000000000000000000000');
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
recordFail(req, username);
audit(req, 'auth.login.fail', { meta: { username, reason: 'no_such_user' } });
return res.status(401).json({ error: 'Invalid credentials' });
}
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const n = recordFail(req, username);
// Mirror counter to DB so admins can see hammered accounts. Best-effort.
pool.query('UPDATE users SET failed_attempts = failed_attempts + 1 WHERE id = $1', [user.id]).catch(() => {});
audit(req, 'auth.login.fail', { targetType: 'user', targetId: user.id, meta: { username, attempt: n } });
return res.status(401).json({ error: 'Invalid credentials' });
}
clearAttempts(req, username);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
pool.query(
'UPDATE users SET failed_attempts = 0, last_login_at = NOW() WHERE id = $1',
[user.id]
).catch(() => {});
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// Regenerate to prevent fixation. Let express-session handle Set-Cookie
// and store-write on res.end — DO NOT call session.save() manually.
req.session.regenerate((err) => {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
if (err) return next(err);
req.session.userId = user.id;
req.session.username = user.username;
req.session.role = user.role;
req.session.isClient = !!user.is_client;
audit(req, 'auth.login.success', { targetType: 'user', targetId: user.id, meta: { username: user.username } });
res.json({
id: user.id,
username: user.username,
display_name: user.display_name,
role: user.role,
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
is_client: !!user.is_client,
});
});
} catch (err) {
next(err);
}
});
// ---------------------------------------------------------------------------
// POST /logout
// ---------------------------------------------------------------------------
router.post('/logout', (req, res, next) => {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const userId = req.session?.userId;
const username = req.session?.username;
req.session.destroy((err) => {
if (err) return next(err);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
res.clearCookie('df.sid');
if (userId) audit(req, 'auth.logout', { targetType: 'user', targetId: userId, meta: { username } });
res.json({ message: 'Logged out' });
});
});
// ---------------------------------------------------------------------------
// GET /me
// ---------------------------------------------------------------------------
router.get('/me', async (req, res) => {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// Auth off → synthetic admin so the app loads in dev / unprotected setups.
if (process.env.AUTH_ENABLED !== 'true') {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const osUser = process.env.LOCAL_OPERATOR || process.env.USER || process.env.USERNAME || 'operator';
return res.json({
id: null,
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
username: osUser.toLowerCase().replace(/[^a-z0-9._-]/g, ''),
display_name: osUser,
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
role: 'admin',
is_client: false,
synthetic: true,
});
}
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
if (!req.session?.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
try {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const r = await pool.query(
'SELECT id, username, display_name, role, is_client, last_login_at FROM users WHERE id = $1',
[req.session.userId]
);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
if (r.rows.length === 0) {
// Session points at a user that no longer exists — drop the session.
req.session.destroy(() => {});
return res.status(401).json({ error: 'User not found' });
}
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
res.json(r.rows[0]);
} catch (err) {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// DB hiccup — fall back to session data so the UI doesn't blank out.
res.json({
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
id: req.session.userId,
username: req.session.username,
role: req.session.role,
is_client: !!req.session.isClient,
});
}
});
fix(auth): make AUTH_ENABLED=true workable end-to-end Three concrete issues kept the login flow broken on dragonflight.live: 1. mam-api trusted no proxy headers, so behind nginx/Cloudflare the session cookie's `secure` flag and the rate-limiter's IP keying both saw the wrong values. Now sets `app.set('trust proxy', 1)`. 2. Session config was tied to NODE_ENV and lacked sameSite/name. Now: - SESSION_COOKIE_SECURE env (default: true when AUTH_ENABLED) so a site behind HTTPS gets Secure cookies regardless of NODE_ENV. - `sameSite: 'lax'` for predictable post-login redirects. - Renamed to `df.sid` so it's obvious in DevTools. - `rolling: true` extends the 7-day TTL on active use. - SESSION_SECRET is now required when AUTH_ENABLED=true; the server refuses to start with a dev default in prod. 3. login.html silently showed the sign-in panel even when no users exist or auth is off: - New GET /auth/setup-status reports {needs_setup, user_count, auth_enabled}. - login.html calls it on load and auto-flips into setup mode when needs_setup is true, or shows an explicit "auth is off" flash when auth_enabled is false (the previous symptom: logout button did nothing because /auth/me returned a synthetic admin no matter what). - Added a `.flash.info` style for the new neutral notice. 4. Sidebar logout used to call /auth/logout then `window.location .reload()`. With auth off that reload landed back on the synthetic- admin app and looked like nothing happened. It now redirects to /login.html in all states so the operator sees feedback (and the server-side messaging about auth being off) instead of a no-op. Deploy notes for zampp1: - Set AUTH_ENABLED=true and a random SESSION_SECRET in the mam-api environment (e.g. /opt/wild-dragon/.env). - Restart mam-api. - First load of /login.html will auto-route to the setup form so you can create the first admin.
2026-05-26 22:47:09 -04:00
// ---------------------------------------------------------------------------
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// GET /setup-status — front-end hint for login.html
fix(auth): make AUTH_ENABLED=true workable end-to-end Three concrete issues kept the login flow broken on dragonflight.live: 1. mam-api trusted no proxy headers, so behind nginx/Cloudflare the session cookie's `secure` flag and the rate-limiter's IP keying both saw the wrong values. Now sets `app.set('trust proxy', 1)`. 2. Session config was tied to NODE_ENV and lacked sameSite/name. Now: - SESSION_COOKIE_SECURE env (default: true when AUTH_ENABLED) so a site behind HTTPS gets Secure cookies regardless of NODE_ENV. - `sameSite: 'lax'` for predictable post-login redirects. - Renamed to `df.sid` so it's obvious in DevTools. - `rolling: true` extends the 7-day TTL on active use. - SESSION_SECRET is now required when AUTH_ENABLED=true; the server refuses to start with a dev default in prod. 3. login.html silently showed the sign-in panel even when no users exist or auth is off: - New GET /auth/setup-status reports {needs_setup, user_count, auth_enabled}. - login.html calls it on load and auto-flips into setup mode when needs_setup is true, or shows an explicit "auth is off" flash when auth_enabled is false (the previous symptom: logout button did nothing because /auth/me returned a synthetic admin no matter what). - Added a `.flash.info` style for the new neutral notice. 4. Sidebar logout used to call /auth/logout then `window.location .reload()`. With auth off that reload landed back on the synthetic- admin app and looked like nothing happened. It now redirects to /login.html in all states so the operator sees feedback (and the server-side messaging about auth being off) instead of a no-op. Deploy notes for zampp1: - Set AUTH_ENABLED=true and a random SESSION_SECRET in the mam-api environment (e.g. /opt/wild-dragon/.env). - Restart mam-api. - First load of /login.html will auto-route to the setup form so you can create the first admin.
2026-05-26 22:47:09 -04:00
// ---------------------------------------------------------------------------
router.get('/setup-status', async (req, res, next) => {
try {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const r = await pool.query('SELECT COUNT(*)::int AS n FROM users');
const n = r.rows[0].n;
fix(auth): make AUTH_ENABLED=true workable end-to-end Three concrete issues kept the login flow broken on dragonflight.live: 1. mam-api trusted no proxy headers, so behind nginx/Cloudflare the session cookie's `secure` flag and the rate-limiter's IP keying both saw the wrong values. Now sets `app.set('trust proxy', 1)`. 2. Session config was tied to NODE_ENV and lacked sameSite/name. Now: - SESSION_COOKIE_SECURE env (default: true when AUTH_ENABLED) so a site behind HTTPS gets Secure cookies regardless of NODE_ENV. - `sameSite: 'lax'` for predictable post-login redirects. - Renamed to `df.sid` so it's obvious in DevTools. - `rolling: true` extends the 7-day TTL on active use. - SESSION_SECRET is now required when AUTH_ENABLED=true; the server refuses to start with a dev default in prod. 3. login.html silently showed the sign-in panel even when no users exist or auth is off: - New GET /auth/setup-status reports {needs_setup, user_count, auth_enabled}. - login.html calls it on load and auto-flips into setup mode when needs_setup is true, or shows an explicit "auth is off" flash when auth_enabled is false (the previous symptom: logout button did nothing because /auth/me returned a synthetic admin no matter what). - Added a `.flash.info` style for the new neutral notice. 4. Sidebar logout used to call /auth/logout then `window.location .reload()`. With auth off that reload landed back on the synthetic- admin app and looked like nothing happened. It now redirects to /login.html in all states so the operator sees feedback (and the server-side messaging about auth being off) instead of a no-op. Deploy notes for zampp1: - Set AUTH_ENABLED=true and a random SESSION_SECRET in the mam-api environment (e.g. /opt/wild-dragon/.env). - Restart mam-api. - First load of /login.html will auto-route to the setup form so you can create the first admin.
2026-05-26 22:47:09 -04:00
res.json({
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
needs_setup: n === 0,
user_count: n,
auth_enabled: process.env.AUTH_ENABLED === 'true',
fix(auth): make AUTH_ENABLED=true workable end-to-end Three concrete issues kept the login flow broken on dragonflight.live: 1. mam-api trusted no proxy headers, so behind nginx/Cloudflare the session cookie's `secure` flag and the rate-limiter's IP keying both saw the wrong values. Now sets `app.set('trust proxy', 1)`. 2. Session config was tied to NODE_ENV and lacked sameSite/name. Now: - SESSION_COOKIE_SECURE env (default: true when AUTH_ENABLED) so a site behind HTTPS gets Secure cookies regardless of NODE_ENV. - `sameSite: 'lax'` for predictable post-login redirects. - Renamed to `df.sid` so it's obvious in DevTools. - `rolling: true` extends the 7-day TTL on active use. - SESSION_SECRET is now required when AUTH_ENABLED=true; the server refuses to start with a dev default in prod. 3. login.html silently showed the sign-in panel even when no users exist or auth is off: - New GET /auth/setup-status reports {needs_setup, user_count, auth_enabled}. - login.html calls it on load and auto-flips into setup mode when needs_setup is true, or shows an explicit "auth is off" flash when auth_enabled is false (the previous symptom: logout button did nothing because /auth/me returned a synthetic admin no matter what). - Added a `.flash.info` style for the new neutral notice. 4. Sidebar logout used to call /auth/logout then `window.location .reload()`. With auth off that reload landed back on the synthetic- admin app and looked like nothing happened. It now redirects to /login.html in all states so the operator sees feedback (and the server-side messaging about auth being off) instead of a no-op. Deploy notes for zampp1: - Set AUTH_ENABLED=true and a random SESSION_SECRET in the mam-api environment (e.g. /opt/wild-dragon/.env). - Restart mam-api. - First load of /login.html will auto-route to the setup form so you can create the first admin.
2026-05-26 22:47:09 -04:00
});
} catch (err) { next(err); }
});
// ---------------------------------------------------------------------------
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// POST /setup — UI-driven first-admin bootstrap. Disabled once any user exists.
// ---------------------------------------------------------------------------
router.post('/setup', async (req, res, next) => {
try {
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const { username, password, display_name } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const policyErr = checkPassword(password, { username });
if (policyErr) return res.status(400).json({ error: policyErr });
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const count = await pool.query('SELECT COUNT(*)::int AS n FROM users');
if (count.rows[0].n > 0) {
return res.status(403).json({
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
error: 'Setup is already complete. Use an existing admin to add more users.',
});
}
const hash = await bcrypt.hash(password, 12);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const r = await pool.query(
`INSERT INTO users (username, password_hash, display_name, role, is_client)
VALUES ($1, $2, $3, 'admin', FALSE)
RETURNING id, username, display_name, role, is_client`,
[String(username).trim().toLowerCase(), hash, display_name || username]
);
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
const newUser = r.rows[0];
audit(req, 'auth.setup', { targetType: 'user', targetId: newUser.id, meta: { username: newUser.username } });
res.status(201).json(newUser);
} catch (err) {
next(err);
}
});
auth: top-to-bottom rework — local accounts, RBAC + client tag, audit log, env-bootstrap Scope (locked in via planning Q&A): - Identity: local accounts only (PG users table) + existing bearer tokens for headless callers. - Transport: httpOnly cookie session for browser, Bearer for API. - RBAC: admin / editor / viewer roles, plus an orthogonal is_client flag for external (agency, talent, customer) accounts. - Bootstrap: ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD env seed the first admin on a clean install. Set ADMIN_BOOTSTRAP_RESET to force-reset the named user (break-glass). - Rate limit: in-memory, 10 fails per 15min per (IP, username). - Password policy: \u22658 chars, mixed case, digit, symbol; small blocklist of common passwords; cannot equal username. - Self-service: change own display name + password. Everything else (role, is_client, other-user mgmt) is admin only. - Audit log: append-only table, indexed by actor + event_type + created_at, populated by every auth/admin event. Files added: - services/mam-api/src/db/migrations/022-auth-rework.sql users.is_client + last_login_at + failed_attempts; audit_log table with FK to users (ON DELETE SET NULL). - services/mam-api/src/middleware/audit.js Fire-and-forget audit() helper. Caller never awaits, failure logs but never throws — auditing cannot break the request that triggered it. - services/mam-api/src/middleware/passwordPolicy.js Shared checkPassword(pw, { username }) used by setup, user create/update, and self-service password change. - services/mam-api/src/tasks/bootstrapAdmin.js Runs after migrations. No-ops unless ADMIN_BOOTSTRAP_USER + ADMIN_BOOTSTRAP_PASSWORD are set AND (users table empty OR ADMIN_BOOTSTRAP_RESET=true). - services/mam-api/src/routes/audit.js Admin-only GET /audit (paginated, filter by event_type / actor / target / date) and GET /audit/event-types. - services/web-ui/public/modal-account-settings.jsx Profile + Password tabs. Triggered by sidebar user button. Files rewritten: - services/mam-api/src/routes/auth.js - POST /login: regenerate(), no manual save(); audit success/ fail/lockout; updates last_login_at + failed_attempts. - POST /logout: destroys session, audits logout. - GET /me: returns is_client + last_login_at. Synthetic admin when AUTH_ENABLED=false. - GET /setup-status: drives login.html UI state. - POST /setup: blocked once any user exists; password policy. - POST /password: self-service. Requires current pw, runs policy, audits, invalidates other sessions implicitly via users.js if changed by admin. - PATCH /me: self-service display_name update. - services/mam-api/src/routes/users.js - is_client field in create/update/list/get. - Guardrails: cannot delete or demote last admin, cannot delete self, admins cannot be flagged is_client. - Password change invalidates all sessions for that user (DELETE FROM sessions WHERE sess->>'userId' = id). - Audit on every mutation. - Password policy enforced. - services/mam-api/src/middleware/auth.js - requireAuth now exposes req.user.is_client. - New requireRole(["admin","editor"], { rejectClients: true }) helper. Applied to cluster, sdk, capture routes (infra). - Synthetic user when AUTH_ENABLED=false has is_client=false. - services/mam-api/src/index.js - Loads bootstrap admin after migrations. - Wires /api/v1/audit. - Cleans up an earlier comment block. - services/web-ui/public/login.html - Password hint added next to setup-mode password field. - services/web-ui/public/shell.jsx - Sidebar user footer is a button that opens AccountSettings. - CLIENT badge next to role when is_client=true. - Nav filters: clients lose ingest tree + jobs + editor; viewers lose ingest + editor; only admins see the Admin section. Power button hidden when synthetic user. - services/web-ui/public/screens-admin.jsx - Users table: new Client column with inline toggle. - InviteUserModal: Client checkbox + password hint, gated off when role=admin. - Last login column replaces Created in primary view. - CSV export includes client + last_login. - services/web-ui/public/data.jsx - ZAMPP_DATA.ME carries is_client + display_name. - services/web-ui/public/index.html - Loads dist/modal-account-settings.js. - services/web-ui/public/styles-rest.css - .user-row grid widened to 6 columns. - docker-compose.yml - Plumbs SESSION_COOKIE_SECURE + ADMIN_BOOTSTRAP_* env vars. Deploy: cd /opt/wild-dragon git pull origin main # In .env: # AUTH_ENABLED=true # SESSION_SECRET=<openssl rand -hex 48> # ADMIN_BOOTSTRAP_USER=admin # ADMIN_BOOTSTRAP_PASSWORD=<strong> docker compose build mam-api web-ui docker compose up -d --force-recreate --no-deps mam-api web-ui
2026-05-26 23:21:07 -04:00
// ---------------------------------------------------------------------------
// POST /password — self-service password change. Requires current password.
// ---------------------------------------------------------------------------
router.post('/password', async (req, res, next) => {
if (process.env.AUTH_ENABLED !== 'true' || !req.session?.userId) {
return res.status(401).json({ error: 'Sign in required' });
}
const { current_password, new_password } = req.body || {};
if (!current_password || !new_password) {
return res.status(400).json({ error: 'current_password and new_password are required' });
}
try {
const r = await pool.query(
'SELECT id, username, password_hash FROM users WHERE id = $1',
[req.session.userId]
);
const u = r.rows[0];
if (!u) return res.status(401).json({ error: 'Session user not found' });
const ok = await bcrypt.compare(current_password, u.password_hash);
if (!ok) {
audit(req, 'auth.password.change', { targetType: 'user', targetId: u.id, meta: { ok: false, reason: 'wrong_current' } });
return res.status(401).json({ error: 'Current password is incorrect' });
}
const policyErr = checkPassword(new_password, { username: u.username });
if (policyErr) return res.status(400).json({ error: policyErr });
if (current_password === new_password) {
return res.status(400).json({ error: 'New password must differ from current' });
}
const newHash = await bcrypt.hash(new_password, 12);
await pool.query(
'UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2',
[newHash, u.id]
);
audit(req, 'auth.password.change', { targetType: 'user', targetId: u.id, meta: { ok: true } });
res.json({ message: 'Password updated' });
} catch (err) { next(err); }
});
// ---------------------------------------------------------------------------
// PATCH /me — self-service display_name change.
// ---------------------------------------------------------------------------
router.patch('/me', async (req, res, next) => {
if (process.env.AUTH_ENABLED !== 'true' || !req.session?.userId) {
return res.status(401).json({ error: 'Sign in required' });
}
const { display_name } = req.body || {};
if (typeof display_name !== 'string' || !display_name.trim()) {
return res.status(400).json({ error: 'display_name is required' });
}
try {
const r = await pool.query(
`UPDATE users
SET display_name = $1, updated_at = NOW()
WHERE id = $2
RETURNING id, username, display_name, role, is_client`,
[display_name.trim().slice(0, 120), req.session.userId]
);
if (r.rows.length === 0) return res.status(404).json({ error: 'User not found' });
audit(req, 'auth.profile.update', { targetType: 'user', targetId: r.rows[0].id, meta: { display_name } });
res.json(r.rows[0]);
} catch (err) { next(err); }
});
export default router;