Files
2026-06-04 16:20:56 -05:00

3.2 KiB

type, date, project, tags
type date project tags
session 2026-05-27 cpas
cpas
authentication
docker
scrypt
sessions
mpm

CPAS — Authentication, user management, and docs

Summary

Added a login popup, env-bootstrapped admin account, and admin-managed user CRUD to the CPAS Violation Tracker. Replaced the long-standing TODO [CRITICAL #1] (no auth on any route). Updated in-app docs and the Unraid install guide.

What shipped

  • Backend (auth.js, db/database.js, server.js)
    • scrypt password hashing (Node built-in crypto, no new deps)
    • users and sessions tables (sessions = DB-backed, 7-day TTL, hex tokens)
    • bootstrapAdmin() creates/syncs admin from ADMIN_USERNAME/ADMIN_PASSWORD on every startup → password owned by Docker env
    • requireAuth / requireAdmin Express middleware
    • Public: /api/health, POST /api/auth/login. Everything else guarded by app.use('/api', auth.requireAuth).
    • User mgmt: GET/POST /api/users, PATCH /api/users/:id/password, DELETE /api/users/:id (all admin-only). Audit log entries for login attempts and user changes.
  • Frontend (client/src/auth.js, LoginModal.jsx, UserManagementModal.jsx, App.jsx)
    • axios interceptors inject Bearer token from localStorage, auto-logout on 401
    • Auth gate in App: validates stored token via /api/auth/me on load, shows LoginModal if no session
    • Nav: user badge, Logout, and admin-only Users button
  • DockerENV ADMIN_USERNAME=admin and ENV ADMIN_PASSWORD=changeme (placeholder, must override)
  • Docs — in-app admin guide (ReadmeModal.jsx) gained an "Authentication & User Accounts" section; auth moved into Roadmap → Shipped; README_UNRAID_INSTALL.md adds the two new env-var rows + login verify step + troubleshooting for forgotten admin password.

Key decisions

  • Used Node's built-in crypto.scryptSync to avoid adding bcrypt as a dependency. Storage format: scrypt$<saltHex>$<hashHex>. Constant-time compare via crypto.timingSafeEqual.
  • DB-backed sessions instead of JWT or in-memory map → survives container restarts, single source of truth, easy admin revoke if needed.
  • Admin password re-syncs from env on every startup. Trade-off: admin can't change their own password in UI (it would be reverted on restart), but rotating the env var rotates the live credential — clean operational story.
  • Did not wrap window.fetch. All component API calls go through axios; the lone bare fetch('/version.json') hits a public static file. So axios interceptors are sufficient.
  • PDF downloads use axios.get(..., { responseType: 'blob' }), not anchor links → interceptor still attaches the token, no special-case needed.

Loose ends / future work

  • No password-change UI for non-admin users yet (admins reset for them).
  • No expired-session cleanup job (lazy prune only).
  • Only admin/user roles. Supervisor-scoped or read-only roles still open.

Verification status

  • node --check passes on auth.js, server.js, db/database.js.
  • client && npm run build succeeds.
  • Could not run server locallybetter-sqlite3 fails to compile against local Node v24. Builds fine in the Docker node:20-alpine image; full verification requires building and running the container.