Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
177 lines
9.4 KiB
Markdown
177 lines
9.4 KiB
Markdown
---
|
|
phase: 03-event-write-back-pwa-install
|
|
plan: 09
|
|
type: tdd
|
|
wave: 1
|
|
depends_on: []
|
|
gap_closure: true
|
|
autonomous: true
|
|
requirements: [CAL-04, CAL-05, CAL-06]
|
|
files_modified:
|
|
- apps/api/src/routes/events.ts
|
|
- apps/api/tests/routes/events.test.ts
|
|
must_haves:
|
|
truths:
|
|
- 'POST /api/events/create with the exact client CreateEventPayload shape ({title,start,end,allDay,recurrence}) returns 202, not 400'
|
|
- 'PATCH /api/events/:uid/edit with the same client shape returns 202, not 400'
|
|
- 'An authenticated OIDC request (devBypassActive=false) with a known iss+sub resolves to a real users.id and is allowed to write — it does NOT unconditionally 401'
|
|
- 'A request with no dev user and no OIDC session returns 401'
|
|
artifacts:
|
|
- path: apps/api/src/routes/events.ts
|
|
provides: 'Canonical title/start/end zod contract + async OIDC iss/sub→users.id resolution on all 5 handlers'
|
|
contains: 'upsertUser'
|
|
key_links:
|
|
- from: 'apps/api/src/routes/events.ts'
|
|
to: 'apps/api/src/auth/user.ts'
|
|
via: 'upsertUser(iss, sub, email)'
|
|
pattern: "upsertUser\\("
|
|
---
|
|
|
|
<objective>
|
|
Fix the route layer so the write path is reachable at all: align the server zod
|
|
schema to the contract the PWA actually sends (CR-01), and implement the real
|
|
OIDC iss/sub → users.id resolution that all five write/sync handlers stub out as
|
|
a hard 401 today (CR-06). Without this plan every create/edit returns 400 in dev
|
|
and 401 in production — the entire phase acceptance criterion is unreachable.
|
|
|
|
Purpose: make the events router accept real client requests under real Authelia auth.
|
|
Output: an events router whose schema matches `CreateEventPayload` and whose OIDC
|
|
path resolves authenticated members to a DB user via the existing `upsertUser` helper.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/03-event-write-back-pwa-install/03-REVIEW.md
|
|
@.planning/phases/03-event-write-back-pwa-install/03-CONTEXT.md
|
|
@apps/api/src/routes/events.ts
|
|
@apps/api/src/auth/user.ts
|
|
@apps/api/src/routes/me.ts
|
|
@apps/pwa/src/api/client.ts
|
|
</context>
|
|
|
|
<artifacts_this_phase_produces>
|
|
This gap plan introduces NO new exported symbols. It changes the in-module
|
|
`eventFieldsSchema` field names and converts the private `resolveUserId(c)` helper
|
|
into an async `resolveUserId(c): Promise<number | null>` that consults `upsertUser`.
|
|
Downstream gap plans (03-10) read the new field names (`title/start/end`) out of
|
|
`calendarOutbox.payload`.
|
|
</artifacts_this_phase_produces>
|
|
|
|
<tasks>
|
|
|
|
<task type="tdd" tdd="true">
|
|
<name>Task 1: RED+GREEN — adopt the canonical title/start/end contract (CR-01)</name>
|
|
<files>apps/api/src/routes/events.ts, apps/api/tests/routes/events.test.ts</files>
|
|
<read_first>
|
|
- apps/api/src/routes/events.ts (eventFieldsSchema at lines 67-77; create handler ~191; edit handler ~268)
|
|
- apps/pwa/src/api/client.ts (CreateEventPayload at lines 119-128 — the authoritative client shape)
|
|
- apps/api/tests/routes/events.test.ts (existing route tests — they currently pass because they send the SERVER field names; that is the wrong boundary the review flagged)
|
|
- .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md (CR-01)
|
|
</read_first>
|
|
<behavior>
|
|
- RED: a new contract test imports the `CreateEventPayload` TYPE shape from the PWA client (or replicates it literally as `{title,start,end,allDay,recurrence}` with a comment citing client.ts:119-128) and POSTs it to /api/events/create — asserts 202, NOT 400. This fails today because zod requires summary/dtstart/dtend.
|
|
- RED: a second test PATCHes the same shape to /api/events/:uid/edit — asserts 202, NOT 400.
|
|
- GREEN: both pass after the schema is renamed.
|
|
</behavior>
|
|
<action>
|
|
Canonical contract chosen: the SERVER adopts the CLIENT field names `title/start/end`
|
|
(the PWA `CreateEventPayload`, `EventForm.handleSubmit`, and `createEvent`/`updateEvent`
|
|
already send these — adopting them server-side requires zero PWA churn).
|
|
|
|
In events.ts rename `eventFieldsSchema` fields to exactly:
|
|
`title: z.string().min(1).max(255)`, `allDay: z.boolean()`,
|
|
`start: z.string().min(1).max(64)`, `end: z.string().min(1).max(64)`,
|
|
`location: z.string().max(2000).optional()`, `description: z.string().max(2000).optional()`,
|
|
`recurrence: z.enum(['none','daily','weekly','monthly','yearly']).optional()`,
|
|
`calendarUrl: z.string().url().max(1024).optional()`.
|
|
Keep `recurrence` `.optional()` server-side (the client always sends it, but the
|
|
contract drift the review noted resolves either way once names match).
|
|
|
|
The route still stores `payload: JSON.stringify(payload)` unchanged — the worker
|
|
(plan 03-10) now parses `title/start/end` from it. Do NOT introduce summary/dtstart/dtend
|
|
anywhere; do NOT add an internal rename map (the review's "map internally" alternative is
|
|
rejected to keep one canonical name set end-to-end).
|
|
|
|
Add the two contract tests described in <behavior>. Commit RED then GREEN
|
|
(`test(03-09): ...` then `feat(03-09): ...`).
|
|
|
|
</action>
|
|
<verify>
|
|
<automated>cd apps/api && npx vitest run tests/routes/events.test.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- behavior: POST /api/events/create with `{title,start,end,allDay,recurrence}` returns 202.
|
|
- behavior: PATCH /api/events/:uid/edit with the same shape returns 202.
|
|
- source: `grep -n 'summary\|dtstart\|dtend' apps/api/src/routes/events.ts` returns no matches in eventFieldsSchema.
|
|
- test-command: `cd apps/api && npx vitest run tests/routes/events.test.ts` passes.
|
|
</acceptance_criteria>
|
|
<done>The server schema accepts the exact payload the PWA sends; no create/edit is rejected at the validator boundary for field-name drift.</done>
|
|
</task>
|
|
|
|
<task type="tdd" tdd="true">
|
|
<name>Task 2: RED+GREEN — resolve OIDC iss/sub to a real users.id on all 5 handlers (CR-06)</name>
|
|
<files>apps/api/src/routes/events.ts, apps/api/tests/routes/events.test.ts</files>
|
|
<read_first>
|
|
- apps/api/src/routes/events.ts (resolveUserId at lines 50-55; the five 401-stub blocks at ~194-200, ~270-273, ~374-377, ~439-442, ~492-495)
|
|
- apps/api/src/auth/user.ts (upsertUser — the canonical iss/sub→users row helper already used by me.ts)
|
|
- apps/api/src/routes/me.ts (the reference OIDC resolution pattern: getAuth → iss/sub/email → upsertUser)
|
|
- .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md (CR-06)
|
|
</read_first>
|
|
<behavior>
|
|
- RED: a test that simulates the production OIDC path (no dev `c.get('user')`; `getAuth` mocked to return a valid `{iss, sub, email}`) POSTs /api/events/create and asserts the response is 202 AND that the row was attributed to the upserted user id (currentUserId != null). Fails today because the handler returns 401 even when auth is truthy.
|
|
- RED: a test with no dev user and `getAuth` returning null asserts 401 (the genuinely-unauthenticated case still 401s).
|
|
</behavior>
|
|
<action>
|
|
Convert `resolveUserId(c)` to an async helper `async function resolveUserId(c): Promise<number | null>`:
|
|
1. If `c.get('user')` exists (dev bypass), return its `.id` (unchanged).
|
|
2. Else call `await getAuth(c)`. If falsy, return null (caller emits 401).
|
|
3. Else extract `iss = (auth.iss as string) ?? ''`, `sub = auth.sub ?? ''`,
|
|
`email = typeof auth.email === 'string' ? auth.email : undefined`, then
|
|
`const user = await upsertUser(iss, sub, email)` and return `user?.id ?? null`.
|
|
Import `upsertUser` from `../auth/user.js`.
|
|
|
|
In each of the 5 handlers (create, edit, delete, sync-status, writable-calendars)
|
|
replace the `resolveUserId(...)` call + inline getAuth/401 stub block with:
|
|
`const currentUserId = await resolveUserId(c)` then `if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)`.
|
|
Remove every `// For now return 401` stub and the now-redundant inner `getAuth` calls in the handlers.
|
|
Per D-10 identity is oidc_iss+oidc_sub; upsertUser keys on `uniq_oidc_identity`. Return 401 ONLY when no session exists (covered by upsertUser path).
|
|
|
|
Add the two tests in <behavior>. Commit RED then GREEN.
|
|
|
|
</action>
|
|
<verify>
|
|
<automated>cd apps/api && npx vitest run tests/routes/events.test.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- behavior: an OIDC request with known iss+sub resolves currentUserId != null and the write enqueues (202).
|
|
- behavior: a request with neither dev user nor OIDC session returns 401.
|
|
- source: `grep -c 'For now return 401' apps/api/src/routes/events.ts` returns 0.
|
|
- source: `grep -c 'upsertUser' apps/api/src/routes/events.ts` returns >= 1.
|
|
- test-command: `cd apps/api && npx vitest run tests/routes/events.test.ts` passes.
|
|
</acceptance_criteria>
|
|
<done>Authenticated Authelia members resolve to a DB user id on every write/sync/writable-calendars handler in production; only genuinely unauthenticated requests 401.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `cd apps/api && npx vitest run tests/routes/events.test.ts` green.
|
|
- `cd apps/api && npm run build` (or tsc) succeeds with the async resolveUserId signature.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
The events router accepts the real PWA payload and resolves real OIDC members.
|
|
The write path is no longer dead-on-arrival at the route boundary (CR-01, CR-06 closed).
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/03-event-write-back-pwa-install/03-09-SUMMARY.md` when done.
|
|
</output>
|