Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.2 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10-admin-role-settings | 03 | api-admin |
|
|
|
|
|
|
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:
createFastmailClient(email, appPassword)+await client.fetchCalendars()in ONE try/catch → any failure throwsCredentialValidationError(typed; no password detail in the exception)encryptPassword(appPassword)→ AES-256-GCM JSON ciphertextdb.insert(memberCredentials).onDuplicateKeyUpdate(...)→ upsert (UNIQUE(user_id) from 10-01)- Fire-and-forget:
loadClientForUser(userId)→syncCalendar(...)per davCal
apps/api/src/routes/admin.ts:
export const adminRouter = new Hono()immediately followed byadminRouter.use('*', requireAdmin)(Pitfall 9)- Side-effect import of
../auth/devBypass.jsfor 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/503GET /calendars:{ calendars: [{ id, displayName, isShared }] }PUT /calendars/:id/shared: clear allis_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 /credentialadded withmeCredentialSchema(no userId field — Pitfall 6)meNoEchoHookidentical 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.tsshows.use('*', requireAdmin)at line 41 (first executable statement after router creation on line 37): PASSgrep "app.route('/api/admin'" apps/api/src/index.tsconfirms mount: PASSgrep "validateEncryptAndStoreCredential" apps/api/src/routes/admin.tsshows import + call: PASSgrep "validateEncryptAndStoreCredential" apps/api/src/routes/me.tsshows import + call: PASSgrep "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 --noEmitexits 0: PASS- Commits
ac36e10(Task 1),037a7ed(RED),d2f6d5d(GREEN) in git log: PASS