docs(10): capture phase context
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
# Phase 10: Admin Role & Settings - Context
|
||||||
|
|
||||||
|
**Gathered:** 2026-06-12
|
||||||
|
**Status:** Ready for planning
|
||||||
|
|
||||||
|
<domain>
|
||||||
|
## 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 11–12 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.
|
||||||
|
|
||||||
|
**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 credential onboarding (backlog 999.5).
|
||||||
|
</domain>
|
||||||
|
|
||||||
|
<decisions>
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
</decisions>
|
||||||
|
|
||||||
|
<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.ts` — `users` (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.ts` — `encryptPassword` / `decryptPassword` (reuse for ADMIN-01).
|
||||||
|
- `apps/api/src/broker/client.ts` — `createDAVClient` / `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.tsx` — `BrowserRouter` 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 `push` — [[drizzle-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.
|
||||||
|
|
||||||
|
</code_context>
|
||||||
|
|
||||||
|
<specifics>
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
</specifics>
|
||||||
|
|
||||||
|
<deferred>
|
||||||
|
## 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 onboarding** (a member adds their own app password on first login) — backlog 999.5; Phase 10 is admin-managed only.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
</deferred>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 10-admin-role-settings*
|
||||||
|
*Context gathered: 2026-06-12*
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Phase 10: Admin Role & Settings - Discussion Log
|
||||||
|
|
||||||
|
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||||
|
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||||
|
|
||||||
|
**Date:** 2026-06-12
|
||||||
|
**Phase:** 10-admin-role-settings
|
||||||
|
**Areas discussed:** Admin bootstrap, Admin UI entry, App-password/credential scope, Shared-calendar designation, Provider scope, Credential ownership
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin bootstrap
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Env-pinned OIDC sub | `ADMIN_OIDC_SUB` marks operator identity as admin | |
|
||||||
|
| First-login-wins | First user to log in is auto-admin | ✓ |
|
||||||
|
| Everyone admin for now | Flag all members admin, defer gating | |
|
||||||
|
| Manual DB seed | Keep setting `is_admin` by hand | |
|
||||||
|
|
||||||
|
**User's choice:** First-login-wins
|
||||||
|
**Notes:** Chosen because Phase 12's setup wizard precedes it — the first login *after setup completes* becomes admin. Must stay member-count-agnostic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin UI entry
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Extend SettingsSheet | Add an Admin section to the existing avatar sheet | |
|
||||||
|
| New /admin route | Dedicated gated route, redirect non-admins | ✓ |
|
||||||
|
| Settings route + sheet stays | Separate gated /admin, keep notifications sheet | |
|
||||||
|
|
||||||
|
**User's choice:** New /admin route
|
||||||
|
**Notes:** Existing notifications `SettingsSheet` stays; `/admin` gives Phase 12's wizard room on the same surface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App-password / credential scope
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Per-member rows, admin edits any | Per-user credentials, admin rotates any | (see provider redirect) |
|
||||||
|
| Per-member, self-only + admin override | Members manage own; admin overrides | |
|
||||||
|
| Single shared credential | One household credential | |
|
||||||
|
|
||||||
|
**User's choice:** Redirected — "treat Fastmail like a provider; design generically enough to plug in Gmail/other providers later." Resolved in the Provider-scope and Credential-ownership follow-ups below.
|
||||||
|
**Notes:** Ties into backlog 999.1 (treat-fastmail-as-a-provider).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared-calendar designation
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Exclusive single-select | Pick exactly one shared calendar; clears prior | ✓ |
|
||||||
|
| Multi-select toggles | Mark multiple calendars shared | |
|
||||||
|
|
||||||
|
**User's choice:** Exclusive single-select
|
||||||
|
**Notes:** Matches the "one shared family calendar" core value.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Provider scope (follow-up)
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Generic shape, Fastmail-only impl | Add provider field + generic UI; implement only Fastmail/CalDAV | ✓ |
|
||||||
|
| Fastmail-only, refactor later | Narrow build now, full refactor in 999.1 | |
|
||||||
|
| Full provider abstraction now | Build pluggable layer + 2nd provider | |
|
||||||
|
|
||||||
|
**User's choice:** Generic shape, Fastmail-only impl
|
||||||
|
**Notes:** Gmail/others = wiring left for backlog 999.1. Keeps Phase 10 shippable without scope creep.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credential ownership (follow-up)
|
||||||
|
|
||||||
|
| Option | Description | Selected |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Per-member provider credential | Per-user rows + provider field, N-member ready | ✓ |
|
||||||
|
| Household-level credential | One record for the household | |
|
||||||
|
|
||||||
|
**User's choice:** Per-member provider credential
|
||||||
|
**Notes:** Today both rows hold the same shared Fastmail account (D-16) but model stays N-member/N-provider ready; admin can rotate any member's.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Claude's Discretion
|
||||||
|
|
||||||
|
- Migration packaging — ship the full v1.1 bundle (`is_admin`, `app_config`, `reminder_lead_minutes`) in one Phase-10 migration (generate+migrate, not push).
|
||||||
|
- `app_config` table shape — create with at least `setup_complete`; add keys as needed.
|
||||||
|
- `/api/admin/*` route layout and the `requireAdmin` server middleware shape.
|
||||||
|
- How the dev-bypass user (id 1) acquires `is_admin` for local verification.
|
||||||
|
|
||||||
|
## Deferred Ideas
|
||||||
|
|
||||||
|
- Full multi-provider support (Gmail/other) — backlog 999.1.
|
||||||
|
- Self-service credential onboarding — backlog 999.5.
|
||||||
|
- Admin audit log / health dashboard / user CRUD — out of scope (REQUIREMENTS.md).
|
||||||
|
- Multiple reminders per event — v1.2 stretch.
|
||||||
Reference in New Issue
Block a user