docs: phase 3b.3 user-management session log + roadmap (Phase 3 complete)
All checks were successful
Build and Push Docker Image / build (push) Successful in 57s
All checks were successful
Build and Push Docker Image / build (push) Successful in 57s
This commit is contained in:
@@ -193,8 +193,14 @@ Each phase keeps the system working and is its own spec → plan → build cycle
|
|||||||
worker/type/activity filters, headline totals and breakdowns per worker/activity/type) backed by
|
worker/type/activity filters, headline totals and breakdowns per worker/activity/type) backed by
|
||||||
`GET /api/admin/report` (completed-only, JS aggregation), plus an all-users filtered CSV via
|
`GET /api/admin/report` (completed-only, JS aggregation), plus an all-users filtered CSV via
|
||||||
`GET /api/admin/export` — a shared `buildSessionsCsv(rows, {includeWorker})` now serves both the
|
`GET /api/admin/export` — a shared `buildSessionsCsv(rows, {includeWorker})` now serves both the
|
||||||
worker self-export and the admin export. **3b remaining:** user management
|
worker self-export and the admin export.
|
||||||
(better-auth `/api/auth/admin/*`).
|
**3b·3 implemented** (plan `docs/superpowers/plans/2026-06-24-phase-3b3-user-management.md`):
|
||||||
|
the admin **Gebruikers** screen — list users (role + active/inactive status), create, set role,
|
||||||
|
reset password, deactivate/reactivate — backed by a new `/api/admin/users*` router (a shared
|
||||||
|
`adminGuard` middleware; `role`/`banned` as direct column updates, create + reset-password via
|
||||||
|
better-auth). Server-side lockout guards (no self-deactivate, no self-demote, last-active-admin
|
||||||
|
invariant), `adminGuard` also rejects banned admins, and deactivate revokes the user's sessions.
|
||||||
|
**Phase 3b and the Phase 3 admin panel are complete.**
|
||||||
Activity management (add/edit/delete handelingen + their `insole_types`) was removed from the
|
Activity management (add/edit/delete handelingen + their `insole_types`) was removed from the
|
||||||
worker client in the Phase 2 follow-up because it is admin-only; it must be **ported here**. The
|
worker client in the Phase 2 follow-up because it is admin-only; it must be **ported here**. The
|
||||||
backend already exists (`/api/activities` writes are admin-gated; `useActivities`/the legacy
|
backend already exists (`/api/activities` writes are admin-gated; `useActivities`/the legacy
|
||||||
|
|||||||
94
docs/sessions/2026-06-24-phase-3b3-user-management.md
Normal file
94
docs/sessions/2026-06-24-phase-3b3-user-management.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Session: 2026-06-24 — Phase 3b·3 (User management — Gebruikers)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Let an admin manage workplace logins: list users with role + status, create a user, change a
|
||||||
|
user's role, reset a forgotten password, and deactivate/reactivate an account — without ever losing
|
||||||
|
production history, and with server-side guards that make admin lockout impossible. Third and final
|
||||||
|
Phase 3b cycle; completes the Phase 3 admin panel.
|
||||||
|
Spec: `docs/superpowers/specs/2026-06-24-phase-3b3-user-management-design.md`;
|
||||||
|
plan: `docs/superpowers/plans/2026-06-24-phase-3b3-user-management.md`.
|
||||||
|
Tracked as Plane epic **SL-54** with tasks **SL-55…SL-60**.
|
||||||
|
|
||||||
|
## Scope decisions (brainstorming)
|
||||||
|
|
||||||
|
- **Operations:** create, set role (worker ↔ admin), reset password, deactivate/reactivate.
|
||||||
|
**No hard delete** — the `work_sessions` FK cascades, so deletion would wipe logged history;
|
||||||
|
deactivation (better-auth `banned`) keeps the data and blocks sign-in.
|
||||||
|
- **Lockout guards (server-side):** no self-deactivate, no self-demote, last-active-admin invariant.
|
||||||
|
- **Password floor:** 8 chars (better-auth default) on create + reset. No email reset flow (no mailer).
|
||||||
|
- **No DB migration** — `role`, `banned`, `banReason`, `banExpires` already exist on `user`.
|
||||||
|
|
||||||
|
## Work done
|
||||||
|
|
||||||
|
Built via a 6-task TDD **Workflow** (`wptwrno4a`, ultracode) — one commit per task, sequential
|
||||||
|
(dependent, shared tree) — followed by a two-lens adversarial security review, then a hardening pass:
|
||||||
|
|
||||||
|
- **Task 1 — adminGuard + contracts** (`a32406d`). Extracted the inline `/api/admin/*` gate into a
|
||||||
|
reusable `adminGuard` middleware in `lib/require-user.ts` (used by both admin routers). Added
|
||||||
|
`UserStatus`, `status` on `AdminUser`, `CreateUserInput`, `SetRoleInput`, `SetPasswordInput` to
|
||||||
|
`@solelog/shared`.
|
||||||
|
- **Task 2 — admin-users router: list + create** (`2b4b633`). New `routes/admin-users.ts`. Enriched
|
||||||
|
`GET /api/admin/users` (id/email/name/role/status/created_at), **moved out of `admin.ts`** so the
|
||||||
|
path is defined once; the report/session pickers keep working (they read id/name). `POST
|
||||||
|
/api/admin/users` via `auth.api.createUser` (hashing), duplicate email → 409. Mounted in `app.ts`.
|
||||||
|
- **Task 3 — set role + guards** (`88b7773`). `POST /:id/role` as a direct column update, with the
|
||||||
|
`activeAdminsExcluding` helper, self-demote guard, and last-admin guard.
|
||||||
|
- **Task 4 — deactivate/reactivate** (`40977f8`). `POST /:id/deactivate` sets `banned=true` **and
|
||||||
|
deletes the user's `session` rows** (kills live bearer tokens); `POST /:id/reactivate` clears it.
|
||||||
|
Self-deactivate + last-admin guards.
|
||||||
|
- **Task 5 — reset password** (`4b80898`). `POST /:id/password` via `auth.api.setUserPassword`
|
||||||
|
(shape verified against the installed better-auth types: `{ userId, newPassword }`).
|
||||||
|
- **Task 6 — Gebruikers UI** (`0f42960`). `api/users.ts` (`useUsers` + 5 mutations), `UserForm.tsx`
|
||||||
|
(create), `Users.tsx` (list with role/status pills, own-row "jij" badge hiding role/deactivate,
|
||||||
|
row actions for role toggle / inline password reset / deactivate-reactivate). Sidebar moves
|
||||||
|
Gebruikers into nav and drops the now-empty "Binnenkort" block; `/gebruikers` route added.
|
||||||
|
|
||||||
|
## Adversarial security review (two lenses, read-only)
|
||||||
|
|
||||||
|
Both lenses returned **no exploitable holes**: `lastAdminProtected`, `gatingConfirmed`,
|
||||||
|
`deactivateBlocksSignin` all true. Key confirmations: no reachable sequence reaches zero active
|
||||||
|
admins (the caller is always an active admin ≠ target, so `activeAdminsExcluding(target) ≥ 1`, and
|
||||||
|
the self-guards stop the caller removing their own admin/active status); deactivate genuinely blocks
|
||||||
|
sign-in (better-auth throws `BANNED_USER` in `session.create.before`, `banExpires=null` prevents
|
||||||
|
auto-unban) **and** the session-row deletion is the load-bearing half that revokes live tokens
|
||||||
|
(core session validation does not re-check `banned`); every `/api/admin/users*` route is gated.
|
||||||
|
|
||||||
|
It surfaced three real, fixable issues, addressed in a hardening pass (`b9aa24c`):
|
||||||
|
|
||||||
|
1. **Test quality** — the two "last active admin" tests asserted `200`/the self-guard, never the
|
||||||
|
last-admin branch (which is unreachable over HTTP). Renamed them honestly and added a **direct
|
||||||
|
unit test of `activeAdminsExcluding`** (covers the `!banned` filter + exclusion).
|
||||||
|
2. **500 on mixed-case create** — create re-selected by original-case email, but better-auth
|
||||||
|
lowercases on store → `toListItem(undefined)`. Now re-selects on `email.toLowerCase()`; added a
|
||||||
|
mixed-case create test.
|
||||||
|
3. **Defense-in-depth** — `adminGuard` checked role only. It now also **rejects banned admins**
|
||||||
|
(so a banned admin holding a live token is refused even if their session somehow survives);
|
||||||
|
added a banned-admin-guard test.
|
||||||
|
|
||||||
|
## Verification (independent of the workflow's self-reports)
|
||||||
|
|
||||||
|
- `git log` — six task commits `a32406d → 0f42960` + hardening `b9aa24c`; **clean tree**.
|
||||||
|
- `yarn workspace @solelog/api test` — **103 passed** (15 files), incl. `admin-users` and the
|
||||||
|
regression `admin`/`report`/`export`/`csv` suites; `typecheck` clean.
|
||||||
|
- `yarn workspace @solelog/admin test` — **50 passed** (11 files), incl. `Users`; `typecheck`
|
||||||
|
clean; `build` succeeds (vite, 94 modules).
|
||||||
|
- `npx oxlint` — clean (exit 0).
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Phase 3b·3 is implemented, hardened, and green across both touched workspaces. An admin can list
|
||||||
|
users with role + status, create workers/admins (who can then sign in), flip roles, reset passwords
|
||||||
|
(new works / old fails), and deactivate/reactivate accounts — with sign-in genuinely blocked on
|
||||||
|
deactivation and live tokens revoked, and with lockout made impossible by the self/last-admin guards
|
||||||
|
plus the banned-admin gate. No DB migration; no hard delete. Plane SL-54 + SL-55…SL-60 all Done.
|
||||||
|
**Phase 3b and the Phase 3 admin panel are complete.**
|
||||||
|
|
||||||
|
`origin/main` was advanced by the maintainer between cycles; after this cycle `main` is **9 ahead**
|
||||||
|
(the whole 3b·3 batch). A single `git push origin main` from the maintainer's terminal ships it
|
||||||
|
(and triggers Gitea CI).
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
Phase 3 is done. Per the roadmap, **Phase 4 — Workbench scanning** (QR at the bench → pre-fill the
|
||||||
|
session, with manual fallback) and **Phase 5 — Polish & deploy** remain.
|
||||||
Reference in New Issue
Block a user