Files
solelog/docs/sessions/2026-06-17-phase-2-accounts-roles.md

4.7 KiB

Session: 2026-06-17 — Phase 2 (Accounts & roles)

Goal

Add worker/admin roles, admin-creates-users, and role-based data scoping to the backend. Backend-only this phase; the React admin panel stays Phase 3.

Decisions (confirmed by maintainer this session)

  1. Role mechanism → better-auth admin() plugin (defaultRole: 'worker', adminRoles: ['admin']). Gives createUser / listUsers / setRole server+client APIs with access control for free (roadmap Decision #6 "don't hand-roll security").
  2. Public sign-upclosed (emailAndPassword.disableSignUp: true). Admin creates users. Worker client becomes login-only (Registreren toggle removed). Dev seed still creates a dev worker and a dev admin via auth.api.createUser (bypasses disableSignUp server-side).
  3. Phase 2 client scope → backend-only. Admin UI is Phase 3. Only worker-client change: drop self-signup.

Key better-auth facts established (read from installed v1.6.18 source)

  • admin/schema.mjs: plugin adds user.role/banned/banReason/banExpires + session.impersonatedBy.
  • admin/routes.mjs createUser: if (!session && (ctx.request || ctx.headers)) throw UNAUTHORIZED → calling auth.api.createUser({ body }) with no headers skips the admin check ⇒ usable for seeding/tests.
  • api/routes/sign-up.mjs:143: throws BAD_REQUEST when emailAndPassword.disableSignUp is set (sign-in unaffected). createUser is a separate endpoint, so it still works.
  • Admin endpoints auto-mount under /api/auth/admin/* via the existing /api/auth/* handler.

Work done

Implemented Phase 2 task-by-task per docs/plans/phase-2-accounts-roles.md (TDD throughout):

  • Task 1 — Shared contracts. Added Role enum (worker | admin), optional user_name / user_email on WorkSession (admin cross-user joins only), and an AdminUser contract in packages/shared/src/index.ts.
  • Task 2 — Test helpers. Centralized auth on apps/api/test/helpers.ts (createTestUser / authToken / bearer / seedActivity) via server-side auth.api.createUser, removing every test's dependency on the public sign-up route so it can be closed.
  • Task 3 — Admin plugin + close sign-up. Wired better-auth's admin() plugin (defaultRole: 'worker', adminRoles: ['admin']), set disableSignUp: true, added the admin-plugin columns (user.role/banned/banReason/banExpires, session.impersonatedBy) and migration 0002. New test asserts public sign-up is rejected.
  • Task 4 — Role-aware helper + activity lockdown. getSessionUser now returns role; added isAdmin; extracted toWorkSession into apps/api/src/lib/work-session.ts; gated activity POST/PUT/DELETE to admins (GET stays open to any authenticated user).
  • Task 5 — Admin router. apps/api/src/routes/admin.ts exposes admin-only GET /api/admin/sessions and /api/admin/sessions/active (all users, joined with activity + user name/email); 401 unauthenticated, 403 non-admin.
  • Task 6 — Dev seed. Seeds dev worker worker@solelog.local + dev admin admin@solelog.local via auth.api.createUser, dev-only and idempotent.
  • Task 7 — Worker client. Removed self-signup: dropped signUp from the API client and AuthContext, made Login login-only (no Registreren toggle).
  • Task 8 — Docs, lint, verification. Updated this log, the roadmap status, and the worker README (both dev logins; self-registration closed). Ran lint/format/typecheck and both test suites green, plus the live HTTP smoke test proving the role rules.

Follow-up (maintainer feedback, same day)

Two UX gaps surfaced once Phase 2 landed, both fixed in commit 1631c16:

  1. No logout — the worker client never surfaced AuthContext.signOut.
  2. Workers saw uneditable Settings — the Instellingen tab was activity management, which Phase 2 made admin-only, so every add/edit/delete 403'd for workers.

Decision (confirmed): the worker app stays worker-only. Replaced the Instellingen tab with an Account screen (signed-in name/email via /api/me + an Uitloggen button), deleted the activity-management Settings screen and its now-unused mutation hooks (useCreateActivity/useUpdateActivity/useDeleteActivity; useActivities read stays for the Stopwatch picker). Activity management belongs to the Phase 3 admin app. No backend change. Worker: 22 tests + typecheck + vite build green.

Plane

  • Epic + tasks created under SoleLog (SL). See plan doc for the mapping.
  • Follow-up item (logout + Account screen) created and marked Done.

Next

Run the build (workflow), verify live (admin can manage users + see all sessions; worker cannot; sign-up closed), then update roadmap status to Phase 2 = Done.