docs(10-03): complete admin-api-surface plan summary and state update

This commit is contained in:
Lucas Berger
2026-06-13 14:57:30 -04:00
parent d2f6d5d77b
commit 0f41993a95
3 changed files with 147 additions and 9 deletions
+2 -2
View File
@@ -181,7 +181,7 @@ Plans:
**Wave 3** *(blocked on Wave 2 completion)* **Wave 3** *(blocked on Wave 2 completion)*
- [ ] 10-03-PLAN.md — adminRouter (members/credentials/calendars/shared) + member self-service credential, validate→encrypt→sync (TDD) - [x] 10-03-PLAN.md — adminRouter (members/credentials/calendars/shared) + member self-service credential, validate→encrypt→sync (TDD)
**Wave 4** *(blocked on Wave 3 completion)* **Wave 4** *(blocked on Wave 3 completion)*
@@ -369,7 +369,7 @@ Plans:
| 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 | | 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 |
| 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 | | 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 |
| 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 | | 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 |
| 10. Admin Role & Settings | v1.1 | 2/4 | In Progress| | | 10. Admin Role & Settings | v1.1 | 3/4 | In Progress| |
| 11. Per-Event Reminders | v1.1 | 0/? | Not started | - | | 11. Per-Event Reminders | v1.1 | 0/? | Not started | - |
| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - | | 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - |
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 | | 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
+8 -7
View File
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.1 milestone: v1.1
milestone_name: Operability & Polish milestone_name: Operability & Polish
status: executing status: executing
stopped_at: "Completed Phase 10 Plan 02 (admin role primitives: requireAdmin, upsertUser is_admin, /api/me isAdmin+needsProviderSetup)" stopped_at: "Completed Phase 10 Plan 03 (admin API surface: requireAdmin-gated adminRouter, shared validateEncryptAndStoreCredential, self-service /api/me/credential)"
last_updated: "2026-06-13T18:39:38.129Z" last_updated: "2026-06-13T18:57:06.660Z"
last_activity: 2026-06-13 -- Phase 10 execution started last_activity: 2026-06-13
progress: progress:
total_phases: 20 total_phases: 20
completed_phases: 7 completed_phases: 7
total_plans: 27 total_plans: 27
completed_plans: 25 completed_plans: 26
percent: 35 percent: 35
--- ---
@@ -26,9 +26,9 @@ See: .planning/PROJECT.md (updated 2026-06-10)
## Current Position ## Current Position
Phase: 10 (admin-role-settings) — EXECUTING Phase: 10 (admin-role-settings) — EXECUTING
Plan: 3 of 4 Plan: 4 of 4
Status: Ready to execute Status: Ready to execute
Last activity: 2026-06-13 -- Phase 10 execution started Last activity: 2026-06-13
### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action) ### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action)
@@ -102,6 +102,7 @@ _Updated after each plan completion_
| Phase 16 P05 | 7 | 2 tasks | 1 files | | Phase 16 P05 | 7 | 2 tasks | 1 files |
| Phase 10-admin-role-settings P01 | 265 | - tasks | - files | | Phase 10-admin-role-settings P01 | 265 | - tasks | - files |
| Phase 10-admin-role-settings P02 | 700 | 3 tasks | 6 files | | Phase 10-admin-role-settings P02 | 700 | 3 tasks | 6 files |
| Phase 10-admin-role-settings P03 | 720 | 3 tasks | 6 files |
## Accumulated Context ## Accumulated Context
@@ -239,7 +240,7 @@ Recent decisions affecting current work:
## Session Continuity ## Session Continuity
Last session: 2026-06-13T18:39:38.119Z Last session: 2026-06-13T18:57:06.634Z
Stopped at: Completed Phase 10 Plan 02 (admin role primitives: requireAdmin, upsertUser is_admin, /api/me isAdmin+needsProviderSetup) Stopped at: Completed Phase 10 Plan 02 (admin role primitives: requireAdmin, upsertUser is_admin, /api/me isAdmin+needsProviderSetup)
Resume file: None Resume file: None
@@ -0,0 +1,137 @@
---
phase: "10-admin-role-settings"
plan: "03"
subsystem: "api-admin"
tags: ["admin", "credentials", "caldav", "encryption", "tdd", "requireAdmin", "self-service", "no-echo", "pitfall-7", "pitfall-9"]
dependency_graph:
requires:
- "users.is_admin column (10-01)"
- "member_credentials.UNIQUE(user_id) constraint (10-01)"
- "requireAdmin MiddlewareHandler (10-02)"
- "isAdmin + needsProviderSetup on /api/me (10-02)"
- "loadClientForUser + triggerTargetedResync in outboxWorker (this plan, Task 1)"
provides:
- "export loadClientForUser from outboxWorker.ts"
- "export triggerTargetedResync from outboxWorker.ts"
- "validateEncryptAndStoreCredential(userId, email, appPassword, providerType) in credentialSync.ts"
- "CredentialValidationError typed failure signal in credentialSync.ts"
- "adminRouter with requireAdmin guard-first, GET /members, POST /credentials, GET /calendars, PUT /calendars/:id/shared"
- "app.route('/api/admin', adminRouter) mount in index.ts"
- "POST /api/me/credential member-scoped self-service endpoint"
- "Integration tests: admin.test.ts (17 test cases)"
affects:
- "Phase 10 Plan 04 (PWA admin UI consumes these routes)"
- "Phase 12 (Setup Wizard reuses /api/admin/credentials and validateEncryptAndStoreCredential)"
tech_stack:
added: []
patterns:
- "Shared validate→encrypt→store→sync helper (credentialSync.ts) imported by both admin and me routes"
- "noEchoHook on zValidator for credential routes — returns { error: 'Invalid request' } only"
- "CredentialValidationError typed exception for all PROPFIND/auth failure modes"
- "adminRouter.use('*', requireAdmin) as first statement (Pitfall 9)"
- "exclusive is_shared update via two sequential Drizzle UPDATEs (Pattern 7)"
- "fire-and-forget initial-sync via loadClientForUser + syncCalendar per davCal"
- "TDD RED (test(10-03)) → GREEN (feat(10-03)) commit discipline"
key_files:
created:
- "apps/api/src/broker/credentialSync.ts"
- "apps/api/src/routes/admin.ts"
- "apps/api/tests/routes/admin.test.ts"
modified:
- "apps/api/src/broker/outboxWorker.ts"
- "apps/api/src/routes/me.ts"
- "apps/api/src/index.ts"
decisions:
- "credentialSync.ts wraps BOTH createFastmailClient AND fetchCalendars in ONE try/catch — any failure from either is a CredentialValidationError; routes map to generic 400 (all failure modes indistinguishable per Pitfall 7)"
- "fire-and-forget initial sync uses davCalendars from the PROPFIND step if available, falling back to loadClientForUser + fetchCalendars — avoids a second PROPFIND round-trip when cals are already known"
- "noEchoHook returns { error: 'Invalid request' } 400 — never result.error (which contains .received = submitted password)"
- "POST /api/me/credential meCredentialSchema excludes userId field (Pitfall 6 / T-10-12) — resolveUserId from session only"
- "adminRouter.use('*', requireAdmin) is first executable statement after export const adminRouter = new Hono()"
metrics:
duration_seconds: 720
completed_date: "2026-06-13"
tasks_completed: 3
files_modified: 6
---
# Phase 10 Plan 03: Admin API Surface + Shared Credential Helper Summary
**One-liner:** Single shared `validateEncryptAndStoreCredential` helper (CalDAV PROPFIND + AES-256-GCM encrypt + upsert + fire-and-forget sync) consumed by `adminRouter` (requireAdmin-first, ADMIN-01/02/03) and `/api/me/credential` self-service (D-07, member-scoped).
## Tasks Completed
| Task | Name | Commits | Files |
|------|------|---------|-------|
| 1 | Promote broker resync helpers to exports | ac36e10 | apps/api/src/broker/outboxWorker.ts |
| 2 RED | Write failing tests for admin surface | 037a7ed | apps/api/tests/routes/admin.test.ts |
| 2+3 GREEN | credentialSync helper + adminRouter + /api/me/credential | d2f6d5d | credentialSync.ts, admin.ts, index.ts, me.ts |
## What Was Built
### Task 1: Promote broker resync helpers to exports
Added `export` keyword to `loadClientForUser` (line 271) and `triggerTargetedResync` (line 302) in `outboxWorker.ts`. Function bodies are byte-for-byte unchanged — only the visibility changed. The outbox drain cycle and `setInterval` scheduling are untouched. No `node-cron` reintroduced.
### Task 2+3: RED → GREEN
**RED:** `apps/api/tests/routes/admin.test.ts` created with 17 test cases covering:
- T-10-08 (Pitfall 9): GET /api/admin/members, POST /credentials, GET /calendars, PUT /calendars/:id/shared all return 403 for non-admin
- T-10-09 (Pitfall 7): POST /api/admin/credentials with PROPFIND auth failure, createFastmailClient throw, network error → all return 400 `{ error: 'Invalid request' }` with the submitted password string absent from the response
- T-10-09: zValidator schema failure → same generic 400, no Zod .received echo
- T-10-11: valid credential → 200, stored AES-256-GCM encrypted (not plaintext)
- ADMIN-02: PUT /api/admin/calendars/:id/shared → exactly one calendar has is_shared=1
- T-10-12 (Pitfall 6): POST /api/me/credential with body userId for another user → credential written only to session user
- D-07: non-admin member can POST /api/me/credential (no requireAdmin required)
All 17 tests confirmed RED (404/assertion failures) before implementation.
**GREEN:**
`apps/api/src/broker/credentialSync.ts` — shared helper:
1. `createFastmailClient(email, appPassword)` + `await client.fetchCalendars()` in ONE try/catch → any failure throws `CredentialValidationError` (typed; no password detail in the exception)
2. `encryptPassword(appPassword)` → AES-256-GCM JSON ciphertext
3. `db.insert(memberCredentials).onDuplicateKeyUpdate(...)` → upsert (UNIQUE(user_id) from 10-01)
4. Fire-and-forget: `loadClientForUser(userId)``syncCalendar(...)` per davCal
`apps/api/src/routes/admin.ts`:
- `export const adminRouter = new Hono()` immediately followed by `adminRouter.use('*', requireAdmin)` (Pitfall 9)
- Side-effect import of `../auth/devBypass.js` for ContextVariableMap
- `GET /members`: users LEFT JOIN member_credentials → `{ members: [{ id, displayName, color, hasCredential }] }`
- `POST /credentials`: `zValidator('json', credentialSchema, noEchoHook)``validateEncryptAndStoreCredential(body.userId, ...)` → 200 or 400/503
- `GET /calendars`: `{ calendars: [{ id, displayName, isShared }] }`
- `PUT /calendars/:id/shared`: clear all `is_shared=true`, set target → 200
`apps/api/src/index.ts`: `app.route('/api/admin', adminRouter)` added after existing route block.
`apps/api/src/routes/me.ts`:
- `POST /credential` added with `meCredentialSchema` (no userId field — Pitfall 6)
- `meNoEchoHook` identical pattern to admin noEchoHook
- Handler: `resolveUserId(c)` from session → `validateEncryptAndStoreCredential(currentUserId, ...)` — body userId ignored
## Deviations from Plan
None — plan executed exactly as written.
## Known Stubs
None. All routes are fully implemented with real DB and real CalDAV integration (mocked in tests). No placeholder data.
## Threat Flags
No new threat surface beyond the plan's threat model. All T-10-08 through T-10-13 mitigations implemented:
- T-10-08: adminRouter.use('*', requireAdmin) guard-first
- T-10-09: noEchoHook + CredentialValidationError → generic 400
- T-10-10: no console.log of body/password in admin.ts, me.ts, or credentialSync.ts
- T-10-11: encryptPassword applied before DB write; tests assert encrypted != plaintext
- T-10-12: /api/me/credential uses currentUserId from session exclusively; test proves other user's row is untouched
## Self-Check: PASSED
- `grep -n "adminRouter.use" apps/api/src/routes/admin.ts` shows `.use('*', requireAdmin)` at line 41 (first executable statement after router creation on line 37): PASS
- `grep "app.route('/api/admin'" apps/api/src/index.ts` confirms mount: PASS
- `grep "validateEncryptAndStoreCredential" apps/api/src/routes/admin.ts` shows import + call: PASS
- `grep "validateEncryptAndStoreCredential" apps/api/src/routes/me.ts` shows import + call: PASS
- `grep "createFastmailClient" apps/api/src/routes/admin.ts apps/api/src/routes/me.ts` — only in a comment (not in route executable code): PASS
- 270/270 tests pass (27 test files): PASS
- `pnpm --filter @familysync/api exec tsc --noEmit` exits 0: PASS
- Commits ac36e10 (Task 1), 037a7ed (RED), d2f6d5d (GREEN) in git log: PASS