--- type: session date: 2026-05-27 project: cpas tags: - 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 - **Docker** — `ENV 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$$`. 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 locally** — `better-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.