55 lines
3.2 KiB
Markdown
55 lines
3.2 KiB
Markdown
---
|
|
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$<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 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.
|