feat(10-03): implement credentialSync helper, adminRouter, and self-service /api/me/credential

credentialSync.ts:
- validateEncryptAndStoreCredential(userId, email, appPassword, providerType) — single
  shared validate→encrypt→store→initial-sync path used by BOTH admin and self-service
- createFastmailClient + fetchCalendars wrapped in ONE try/catch: any failure throws
  CredentialValidationError (routes map to { error: 'Invalid request' } 400)
- appPassword never logged or echoed (T-10-10)
- encryptPassword (AES-256-GCM) applied before DB write (T-10-11)
- fire-and-forget initial sync via loadClientForUser + syncCalendar (Pitfall 5)

admin.ts:
- adminRouter.use('*', requireAdmin) FIRST (Pitfall 9 / T-10-08)
- GET /members: users LEFT JOIN member_credentials → hasCredential boolean
- POST /credentials: noEchoHook + validateEncryptAndStoreCredential (T-10-09)
- GET /calendars: calendar list (UI-SPEC Surface 5)
- PUT /calendars/:id/shared: exclusive is_shared update (ADMIN-02, D-06)

index.ts:
- app.route('/api/admin', adminRouter) mounted in route block

me.ts:
- POST /credential: member self-service, always currentUserId (Pitfall 6 / T-10-12)
- meCredentialSchema (no userId field), meNoEchoHook, calls shared helper
- All 17 new admin tests pass; 270 total pass; tsc --noEmit clean
This commit is contained in:
Lucas Berger
2026-06-13 14:54:23 -04:00
parent 037a7ed4c1
commit d2f6d5d77b
4 changed files with 360 additions and 0 deletions
+2
View File
@@ -9,6 +9,7 @@ import { eventsRouter } from './routes/events.js';
import { sseRouter } from './routes/sse.js';
import { listsRouter, listItemsRouter } from './routes/lists.js';
import { pushRouter } from './routes/push.js';
import { adminRouter } from './routes/admin.js';
import { oidcAuthMiddleware, processOAuthCallback } from './auth/middleware.js';
import { devAuthBypass } from './auth/devBypass.js';
import { persistSessionCookie } from './auth/persistSessionCookie.js';
@@ -70,6 +71,7 @@ app.route('/api/sse', sseRouter);
app.route('/api/lists', listsRouter);
app.route('/api/list-items', listItemsRouter);
app.route('/api/push', pushRouter);
app.route('/api/admin', adminRouter);
// WR-04: background worker startup (cron schedules) moved into the isMainModule()
// guard below. Calling them at top level registered real node-cron schedules whenever