Files
familysync/.planning/phases/20-admin-member-editor-form-declutter/20-02-PLAN.md
T

7.6 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
20-admin-member-editor-form-declutter 02 execute 1
apps/pwa/src/api/client.ts
true
truths artifacts key_links
The PWA can call the member-profile update route and receive a typed result
AdminMember carries isAdmin so the editor toggle can show the correct initial state
A last-admin demotion 409/422 from the server is surfaced as a distinguishable sentinel error
path provides contains
apps/pwa/src/api/client.ts updateMemberProfile fetcher + isAdmin on AdminMember updateMemberProfile
from to via pattern
apps/pwa/src/api/client.ts updateMemberProfile apps/api/src/routes/admin.ts PATCH /members/:id fetch PATCH /api/admin/members/:id api/admin/members/
Extend the PWA API client (`apps/pwa/src/api/client.ts`) with: the `isAdmin: boolean` field on the `AdminMember` type, and a new `updateMemberProfile(memberId, { displayName?, isAdmin? })` fetcher that calls `PATCH /api/admin/members/:id` (the route created in Plan 20-01) and maps the D-03 last-admin 409/422 to a distinguishable sentinel error.

Purpose: Decouples the PWA editor (Plan 20-03) from the wire shape. This is a thin, single-file, glue-code change with no business logic of its own — standard (non-TDD) execution. Output: A typed updateMemberProfile fetcher + AdminMember.isAdmin field consumed by Plan 20-03.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/STATE.md @.planning/phases/20-admin-member-editor-form-declutter/20-CONTEXT.md @.planning/phases/20-admin-member-editor-form-declutter/20-PATTERNS.md

<artifacts_produced>

Artifacts this phase produces (Plan 20-02)

  • isAdmin: boolean field added to the AdminMember interface in apps/pwa/src/api/client.ts
  • updateMemberProfile(memberId, body) fetcher in apps/pwa/src/api/client.ts calling PATCH /api/admin/members/:id
  • A 'last-admin' sentinel Error thrown on 409/422 (mirrors the existing 'conflict' sentinel pattern) </artifacts_produced>
Task 1: Add AdminMember.isAdmin + updateMemberProfile fetcher apps/pwa/src/api/client.ts - apps/pwa/src/api/client.ts (lines ~560-575 AdminMember interface; lines ~184-234 fetchCreateMember + fetchAdminResetPassword as the fetcher analog; the SessionExpiredError + handleAuthResponse conventions used by every fetcher; the existing 'conflict' sentinel at line ~206) - .planning/phases/20-admin-member-editor-form-declutter/20-PATTERNS.md ("apps/pwa/src/api/client.ts" section — type + fetcher excerpts) - .planning/phases/20-admin-member-editor-form-declutter/20-CONTEXT.md (D-02, D-03) - `AdminMember` now has a required `isAdmin: boolean` field. - `updateMemberProfile(7, { displayName: 'X' })` issues `PATCH /api/admin/members/7` with a JSON body, `credentials: 'include'`, `redirect: 'manual'`. - A 401 or `opaqueredirect` response throws `SessionExpiredError` (existing convention). - A 409 or 422 response throws `new Error('last-admin')` (sentinel the editor branches on). - Any other non-ok response throws a generic error. - A 200 resolves void. In apps/pwa/src/api/client.ts: (1) Add `isAdmin: boolean;` to the `AdminMember` interface (after `color`, before `hasCredential`). (2) Add an exported async function `updateMemberProfile(memberId: number, body: { displayName?: string; isAdmin?: boolean }): Promise`. Use the same `fetch` shape as `fetchAdminResetPassword`: method `'PATCH'` to `/api/admin/members/${memberId}`, `Content-Type: application/json`, `credentials: 'include'`, `redirect: 'manual'`, JSON-stringified body. Reuse the file's existing session-expiry handling (`res.type === 'opaqueredirect' || res.status === 401` -> `SessionExpiredError`). Map `res.status === 409 || res.status === 422` to `throw new Error('last-admin')`. Throw a generic error on any other non-ok status. The verb MUST be `PATCH` to match the Plan 20-01 route. cd /home/luc/projects/familysync && grep -n "updateMemberProfile" apps/pwa/src/api/client.ts && grep -nE "isAdmin: *boolean" apps/pwa/src/api/client.ts && pnpm --filter @familysync/pwa exec tsc --noEmit - `grep -n "updateMemberProfile" apps/pwa/src/api/client.ts` returns the exported fetcher. - `grep -n "PATCH" apps/pwa/src/api/client.ts` shows the new fetcher uses the PATCH verb on `/api/admin/members/`. - `AdminMember` interface includes `isAdmin: boolean`. - The 409/422 branch throws an Error whose message is the literal `last-admin` sentinel. - `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0. AdminMember.isAdmin + updateMemberProfile exist, typed, and the PWA typechecks. Task 2: Pass PWA CI gates apps/pwa/src/api/client.ts - apps/pwa/src/api/client.ts (the edits from Task 1) - /home/luc/.claude/projects/-home-luc-projects-familysync/memory/MEMORY.md ("CI checks conformance" entry) Run the PWA eslint + prettier gates on the modified file and fix any violations. Commit: `feat(20-02): add updateMemberProfile fetcher + AdminMember.isAdmin`. cd /home/luc/projects/familysync && pnpm --filter @familysync/pwa exec eslint src/api/client.ts && pnpm exec prettier --check apps/pwa/src/api/client.ts - eslint passes on apps/pwa/src/api/client.ts (exit 0). - prettier --check passes on apps/pwa/src/api/client.ts. - Change committed with a `feat(20-02):` message. PWA lint + format gates pass for client.ts; change committed.

<threat_model>

Trust Boundaries

Boundary Description
PWA -> /api/admin Client fetch crosses into the admin surface; server-side requireAdmin is the real boundary (unchanged). The client merely calls it.

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-20-05 Spoofing (stale session) updateMemberProfile fetch mitigate Reuses the file's SessionExpiredError on 401/opaqueredirect, triggering the existing re-auth flow — no silent failure.
T-20-06 Elevation of Privilege (client trust) last-admin sentinel accept The 409/422 guard is enforced server-side (Plan 20-01); the client only surfaces it. Client-side toggle state is non-authoritative by design (existing pattern: "isAdmin drives nav visibility; real boundary is server-side").
</threat_model>
- `grep -n "updateMemberProfile" apps/pwa/src/api/client.ts` — fetcher present. - `AdminMember` has `isAdmin: boolean`. - PWA typecheck + eslint + prettier pass on client.ts.

<success_criteria>

  • updateMemberProfile calls PATCH /api/admin/members/:id and maps 409/422 to a last-admin sentinel.
  • AdminMember.isAdmin exists and is typed boolean.
  • PWA CI gates pass for the modified file. </success_criteria>
Create `.planning/phases/20-admin-member-editor-form-declutter/20-02-SUMMARY.md` when done.