Files
2026-06-18 22:21:38 -04:00

13 KiB
Raw Permalink Blame History

Phase 10: Admin Role & Settings - Context

Gathered: 2026-06-12 Updated: 2026-06-13 — folded backlog 999.5 self-service onboarding INTO scope (D-07), reconciling with the ROADMAP fold (commit bcc9682, 2026-06-11). The original discuss-phase had listed it as deferred without accounting for that ROADMAP edit. Status: Ready for planning

## Phase Boundary

Phase 10 introduces an admin role and the role-gated Admin Settings surface, and carries the v1.1 DB-foundation migration that Phases 1112 consume.

Delivers:

  • DB foundation: users.is_admin + a new app_config table + calendar_events.reminder_lead_minutes — all three shipped in this phase's migration (per the ROADMAP DB-foundation note: the v1.1 schema migration is "carried by Phase 10"). is_admin and app_config are used here; reminder_lead_minutes is created-now / consumed by Phase 11; app_config.setup_complete is created-now / consumed by Phase 12.
  • Admin role (ADMIN-03): a role check gating admin routes/UI; non-admins cannot reach or invoke them. First-login-wins bootstrap (see D-01).
  • App-password / credential management (ADMIN-01): an admin can view household members and rotate / re-enter a member's provider credential from the UI; validated against the provider (CalDAV PROPFIND for Fastmail) before saving, stored encrypted via the existing crypto.ts / APP_PASSWORD_ENCRYPTION_KEY path; never displayed, logged, or echoed.
  • Shared-calendar designation (ADMIN-02): an admin picks which synced calendar is the shared family calendar (calendars.is_shared) from the UI, replacing the manual DB write.
  • Self-service credential onboarding (folded from backlog 999.5): a member with no member_credentials row gets a needsProviderSetup signal and can enter / validate (CalDAV PROPFIND) / encrypt their own Fastmail app password on first login — the member-scoped counterpart of the admin-managed flow (see D-07), reusing the same validate→encrypt→initial-sync path. Member-scoped: a member can only set their own credential.

NOT in this phase: the Phase 12 setup wizard itself; per-event reminder UI/scheduling (Phase 11); full multi-provider support (backlog 999.1 — only the generic shape lands here, Fastmail/CalDAV is the only implemented provider). (Self-service onboarding from backlog 999.5 is folded IN — see the Delivers bullet above and D-07.)

## Implementation Decisions

Admin bootstrap (ADMIN-03)

  • D-01: First-login-wins. When no admin exists, the first user to log in is flagged is_admin=true; subsequent users are normal members. Chosen deliberately to dovetail with Phase 12: the first login after setup completes becomes the admin. Must stay member-count-agnostic — the flag is a per-user boolean, not a hardcoded single-admin assumption, so more admins can be promoted later.
    • Phase-12 interaction to honor: the "first login" that wins admin should ultimately be the first login after app_config.setup_complete. Phase 12 owns setup_complete; Phase 10 ships the column and the first-login-wins logic. Planner should implement the bootstrap so it reads cleanly once setup_complete gating is layered on in Phase 12 (e.g. "first user when zero admins exist" today, tightened to "first user after setup_complete" in P12) — do not hardcode anything that Phase 12 would have to rip out.
    • Dev note: under DEV_AUTH_BYPASS, DEV_USER (id 1) is injected without a DB upsert. Decide and document how the dev/bypass user acquires is_admin for local admin-UI verification (e.g. seed id 1 as admin, or have the bypass path flag it) — see dev-data-user1-no-calendars.

Admin UI entry & gating

  • D-02: New /admin route. A dedicated gated route (not an extension of the existing notifications SettingsSheet). An is_admin guard redirects non-admins away. Gives Phase 12's wizard room to grow on the same route surface. The existing avatar SettingsSheet (notifications toggle) stays as-is.
  • D-03: Expose is_admin on /api/me. The PWA needs the flag to render/guard the /admin entry; /api/me currently returns only { id, displayName, color } and must add isAdmin. The server still enforces the role on every /api/admin/* route — the client flag is for UX only, never the security boundary (ADMIN-03 is server-enforced).

Credential model (ADMIN-01)

  • D-04: Generic provider shape, Fastmail-only implementation. Add a provider/type discriminator to the credential model and frame the admin UI around "a provider credential" (avoid hardcoded "Fastmail app password" copy in the data model / API). Implement and validate ONLY Fastmail/CalDAV (PROPFIND) now. Gmail/other providers are wiring left for backlog 999.1 — do not build a second provider here. See project-nmember-expansion and backlog 999.1.
  • D-05: Per-member provider credential. Keep member_credentials per-user; each member owns their credential row (today both rows happen to hold the same shared Fastmail account per D-16, but the model stays N-member / N-provider ready). An admin can rotate any member's credential. Reuse the existing encryption path; never expose the plaintext.

Shared-calendar designation (ADMIN-02)

  • D-06: Exclusive single-select. The admin picks exactly one synced calendar as the shared family calendar. Setting a new one clears is_shared on any prior shared calendar (single shared calendar, matching the core value). Picker lists the synced calendars; selection is a radio/toggle, not independent multi-toggles.

Self-service credential onboarding (ADMIN-01 / folded from backlog 999.5)

  • D-07: Member self-service credential, member-scoped. A member with no provider credential gets a needsProviderSetup signal (no member_credentials row) and can enter / validate (CalDAV PROPFIND) / encrypt their own Fastmail app password — the self-service counterpart to the admin-managed rotation (D-05), sharing the identical validate→encrypt→initial-sync path. A member can set ONLY their own credential; the cross-member rotation stays admin-only (D-05). Non-technical-friendly instructions are a hard UX constraint: link to Fastmail's app-password page and name the required Calendars/CalDAV scope. Never log/echo the password. Folded in per the ROADMAP edit (commit bcc9682). Reuses the same generic provider shape as D-04 (Fastmail/CalDAV only; no second provider here).

Claude's Discretion

  • Migration packaging: ship the full v1.1 column/table bundle (is_admin, app_config, reminder_lead_minutes) in one Phase-10 migration per the ROADMAP note, so Phases 11/12 don't each carry their own migration. Use drizzle-kit generate + migrate — never push (see drizzle-mariadb-push-unsafe).
  • app_config shape: create the table now with at least a setup_complete flag (Phase 12). Add other global keys only as Phase 10 actually needs them; keep it a simple key/value or single-row config — planner's call.
  • /api/admin/* route layout: sub-routes for members/credentials and shared-calendar — planner decides exact paths, following the existing routes/*.ts Hono pattern.
  • Server-side admin middleware shape (a requireAdmin guard analogous to the existing auth middleware) — planner's call.

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Requirements & roadmap

  • .planning/REQUIREMENTS.md — ADMIN-01, ADMIN-02, ADMIN-03 (full wording + the "Role-agnostic design" note); the DB-foundation note coupling the v1.1 migration to Phase 10; the deferred items (self-service onboarding 999.5, audit log/user-CRUD out of scope).
  • .planning/ROADMAP.md §Phase 10 — phase goal, success criteria, dependency chain (10 → 11, 10 → 12).
  • .planning/phases/999.1-treat-fastmail-as-a-provider-support-more-calendar-providers/ — the backlog phase that D-04's generic shape is designed to plug into. Read for the provider-abstraction direction so Phase 10's credential model doesn't paint 999.1 into a corner.

Codebase maps

  • .planning/codebase/ARCHITECTURE.md — overall API/PWA architecture + the ordering rationale that folded the migration into Phase 10.
  • .planning/codebase/STRUCTURE.md — where routes / schema / frontend pages live.
  • .planning/codebase/CONVENTIONS.md — naming + module patterns to match.

Key source files

  • apps/api/src/db/schema.tsusers (add is_admin), calendars (is_shared already exists, l.89), member_credentials (add provider discriminator), calendar_events (add reminder_lead_minutes). New app_config table.
  • apps/api/src/broker/crypto.tsencryptPassword / decryptPassword (reuse for ADMIN-01).
  • apps/api/src/broker/client.tscreateDAVClient / fetchCalendars (reuse for CalDAV PROPFIND credential validation).
  • apps/api/src/index.ts — route mounting + auth middleware order (mount /api/admin/* behind the auth guard, add a requireAdmin layer).
  • apps/api/src/routes/me.ts + apps/api/src/auth/devBypass.ts + apps/api/src/auth/user.ts — current-user resolution; where to add isAdmin to the /api/me response and bootstrap the flag on upsert.
  • apps/pwa/src/App.tsxBrowserRouter routes (add gated /admin); SettingsSheet entry pattern.
  • apps/pwa/src/api/client.ts/api/me client type (add isAdmin).

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • crypto.ts (encryptPassword/decryptPassword, AES-256-GCM via APP_PASSWORD_ENCRYPTION_KEY): the exact store path ADMIN-01 must reuse — no new crypto.
  • member_credentials table (per-user, JSON {iv,authTag,ciphertext} + fastmail_email): extend with a provider discriminator rather than replace.
  • broker/client.ts createDAVClient + fetchCalendars: drives CalDAV PROPFIND — reuse to validate a credential before save (SETUP-02 also reuses this in P12).
  • auth/devBypass.ts c.get('user') pattern: how routes read the current user; the requireAdmin guard and admin routes follow the same context-user pattern.
  • SettingsSheet (avatar-opened): existing settings UX precedent; /admin is a sibling, not a replacement.

Established Patterns

  • Routes are per-feature Hono routers under apps/api/src/routes/, mounted in index.ts behind devAuthBypass()oidcAuthMiddleware() on /api/*. Admin routes mount in the same protected band, plus a requireAdmin check.
  • Schema migrations via drizzle-kit generate + migrate (NOT pushdrizzle-mariadb-push-unsafe).
  • Identity is oidc_iss + oidc_sub, never email (D-10); upsertUser is the bootstrap hook for first-login-wins.
  • PWA routing is declarative react-router <Routes> in App.tsx; server state via TanStack Query, UI-only state via Zustand.

Integration Points

  • /api/me response → add isAdmin; PWA meQuery consumers gate the /admin nav entry.
  • upsertUser (auth/user.ts) → first-login-wins flag write.
  • calendars.is_shared write moves from manual DB edit to the ADMIN-02 endpoint; the calendar legend / shared-calendar consumers already read is_shared.
  • app_config.setup_complete → consumed by Phase 12; calendar_events.reminder_lead_minutes → consumed by Phase 11.
  • needsProviderSetup signal (member has no member_credentials row) → surfaced to the PWA (e.g. on /api/me or a dedicated endpoint — planner's call) to drive the member self-service onboarding entry (D-07); reuses the admin flow's validate→encrypt→initial-sync path, member-scoped.

</code_context>

## Specific Ideas
  • The credential UI/data model should read as "provider credential," not "Fastmail app password" — the user explicitly wants Gmail/other providers pluggable later without reshaping the schema (D-04). Fastmail is just the first/only implemented provider.
  • First-login-wins was chosen specifically because Phase 12's setup wizard precedes it: the operator who completes setup is the first to log in and becomes admin (D-01).
## Deferred Ideas
  • Full multi-provider support (Gmail/other) — backlog 999.1. Phase 10 lands only the generic credential shape; actual additional providers + their validation are 999.1.
  • Self-service credential onboardingfolded INTO this phase (D-07) per the ROADMAP edit (commit bcc9682, 2026-06-11); no longer deferred. The original discuss-phase listed it as deferred without accounting for that ROADMAP fold; reconciled 2026-06-13.
  • Admin audit log / health dashboard / user CRUD — explicitly out of scope per REQUIREMENTS.md (scope creep for a small self-hosted app).
  • Multiple reminders per event — v1.2 stretch (noted in REQUIREMENTS.md, unrelated to Phase 10).

Reviewed Todos (not folded)

  • 2026-06-10-gitea-ci-regression-and-docker-publish.md ("Gitea CI — full regression + Docker publish") — matched only on weak generic keywords (build/api/phase); it is Phase 8 work already completed. Not relevant to Phase 10; not folded.

Phase: 10-admin-role-settings Context gathered: 2026-06-12