14 KiB
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-shared-lists-live-sync | 03 | execute | 2 |
|
|
true |
|
|
MVP slice: after this plan a real user can create and delete lists — a capability they did not have after Plan 01's empty shell.
Purpose: Establish the lists router (the analog every later list/item endpoint extends) with correct access control and the auto-share-on-create behavior, plus the lists-index UI. Output: listsRouter mounted at /api/lists; ListsIndex wired to real data; CreateListSheet + ListCard + ListDeleteDialog; listsClient typed functions.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/04-shared-lists-live-sync/04-CONTEXT.md @.planning/phases/04-shared-lists-live-sync/04-RESEARCH.md @.planning/phases/04-shared-lists-live-sync/04-PATTERNS.md @.planning/phases/04-shared-lists-live-sync/04-UI-SPEC.md @.planning/phases/04-shared-lists-live-sync/04-VALIDATION.md Task 1: Lists router — POST/GET/PATCH/DELETE /api/lists with scoped access (LIST-01, D-01/D-02/D-04/D-06) apps/api/src/routes/lists.ts, apps/api/src/routes/lists.test.ts, apps/api/src/index.ts - apps/api/src/routes/events.ts (full — resolveUserId, zod schemas, handler/try-catch/401 conventions) - apps/api/src/routes/lists.test.ts (RED stub from Plan 01) - apps/api/src/index.ts (route mount order) - apps/api/src/auth/user.ts (upsertUser, deriveDisplayName signatures) - apps/api/src/db/schema.ts (lists, listShares, listItems, users) - .planning/phases/04-shared-lists-live-sync/04-PATTERNS.md §"apps/api/src/routes/lists.ts" + §"Shared Patterns" - .planning/phases/04-shared-lists-live-sync/04-RESEARCH.md §"Open Questions" item 3 (auto-populate list_shares) - Test: POST /api/lists { name, isShared:true } inserts a lists row owned by the caller AND inserts list_shares rows for every other user (not the creator). (LIST-01, D-01, Open Question 3) - Test: POST /api/lists { name, isShared:false } inserts the list with NO list_shares rows. - Test: GET /api/lists returns lists where owner_id = caller OR caller is in list_shares; does NOT return another member's private list (D-04 security-critical). - Test: GET /api/lists includes an item-count summary per list (active/done) for the card badge; assert the field is present. - Test: DELETE /api/lists/:id by the owner removes the list and cascades items + shares; a non-owner/non-sharee gets 403; unknown id gets 404. - Test: PATCH /api/lists/:id updates name and/or isShared by an authorized member; toggling isShared false→true (re)populates shares, true→false removes non-owner shares. - Test: zod rejects name > 255 or empty. Create apps/api/src/routes/lists.ts exporting `listsRouter` (Hono). Copy the `resolveUserId` helper verbatim from events.ts (per project convention it is duplicated per router, not extracted). Apply the 401 guard + try/catch-503 conventions on every handler. Define zod schemas: createListSchema (name 1..255, isShared default true), patchListSchema (name?/isShared?, at least one).Implement handlers: POST / (create list; if isShared, query users for all member ids except creator and insert list_shares rows — YAGNI auto-share per Open Question 3); GET / (scoped select: owner_id = caller OR id IN list_shares.userId = caller, returning id/name/isShared/ownerId + per-list item counts); PATCH /:id (authorized update of name/isShared, reconciling list_shares on visibility change); DELETE /:id (owner-only delete is the safe default; cascade handles items/shares). Verify list access with the ownership/share-check pattern from 04-PATTERNS before any mutation.
Mount in index.ts: `import { listsRouter }` and `app.route('/api/lists', listsRouter)` after the sseRouter mount (so it sits behind the OIDC/dev-bypass guard). Do NOT add fan-out emit calls here yet — Plan 06 adds publishListEvent triggers once the SSE endpoint exists (leave a commented seam, note it in SUMMARY). NOTE: per-field item PATCH and item endpoints are Plan 04; this plan is lists only.
Build CreateListSheet.tsx per UI-SPEC: bottom sheet (mobile) / centered modal (desktop), heading "New list", auto-focused name input (placeholder "e.g. Groceries"), Shared/Private toggle defaulting to Shared (D-01), "Create" button (accent var(--color-member-0), disabled while name empty, destructive border on blank-submit attempt), "Cancel". On create: useMutation(createList) with optimistic insert into ['lists'] + onError rollback + onSettled invalidate; close sheet on success. Open/close driven by listsStore.createListSheetOpen.
Build ListCard.tsx per UI-SPEC: rounded card, list name (heading), "N items / N active · M done" badge, "Shared" pill for shared lists (nothing for private), ChevronRight; whole card taps through to /lists/:id via react-router navigate/Link; swipe/long-press (phone) or hover X (desktop) reveals Delete which opens ListDeleteDialog. All user text as plain-text JSX (XSS guard).
Build ListDeleteDialog.tsx by mirroring DeleteConfirmationDialog structure (do NOT modify the existing one — it is wired to calendarStore): same modal layout, backdrop, role="dialog"/aria-modal, Escape-to-close, focus-on-open, CSS tokens; heading "Delete list?", body '"{name}" and all its items will be permanently removed.', Cancel + destructive Delete (D-06). On confirm: useMutation(deleteList) optimistic removal from ['lists'] + navigate back to /lists; failure toast "Couldn't delete. Try again."
Replace the ListsIndex placeholder card stack with real ListCard rendering from useQuery(['lists']); ListsEmptyState when zero lists; FAB ("+ New List") opens CreateListSheet.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| browser → /api/lists | client supplies name/isShared/list id — all untrusted |
| API → MariaDB | scoped queries enforce who can see/mutate a list |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-04-05 | Elevation of Privilege | accessing another member's private list via direct id (GET/DELETE/PATCH /api/lists/:id) | mitigate | Every handler resolves caller via resolveUserId and verifies owner_id OR list_shares before returning/mutating; 403 otherwise; tested |
| T-04-02 | Information Disclosure | GET /api/lists leaking non-shared lists | mitigate | Scoped WHERE owner_id = caller OR id IN list_shares; negative test asserts another member's private list is absent (D-04) |
| T-04-06 | Tampering | XSS via list name | mitigate | List names rendered as plain-text JSX children only; no dangerouslySetInnerHTML (T-03-15 pattern) |
| T-04-07 | Tampering | overposting on PATCH (fields beyond name/isShared) | mitigate | zod patchListSchema whitelists name/isShared only |
| T-04-08 | Elevation of Privilege | self-adding to list_shares | mitigate | Shares are server-managed only (auto-populated on create/visibility change); no client-writable shares endpoint exposed in Phase 4 |
| </threat_model> |
<success_criteria>
- LIST-01 satisfied: create + delete named lists end to end.
- Shared-by-default with server-managed list_shares; scoped GET enforced.
- listsRouter is the analog later item/SSE plans extend. </success_criteria>
<artifacts_produced> Symbols/files this plan creates (exclude from drift verification):
apps/api/src/routes/lists.tsexportinglistsRouter(POST/GET/PATCH/DELETE /api/lists); localresolveUserIdcopyapp.route('/api/lists', listsRouter)mount in apps/api/src/index.tsapps/pwa/src/api/listsClient.ts:fetchLists,createList,patchList,deleteList, typesList,ListItem- Components:
CreateListSheet,ListCard,ListDeleteDialog,ListsEmptyState - Real-data
ListsIndex(replaces Plan 01 placeholder) </artifacts_produced>