docs(20): create phase plan
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
---
|
||||
phase: 20-admin-member-editor-form-declutter
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/pwa/src/api/client.ts
|
||||
autonomous: true
|
||||
requirements: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "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"
|
||||
artifacts:
|
||||
- path: "apps/pwa/src/api/client.ts"
|
||||
provides: "updateMemberProfile fetcher + isAdmin on AdminMember"
|
||||
contains: "updateMemberProfile"
|
||||
key_links:
|
||||
- from: "apps/pwa/src/api/client.ts updateMemberProfile"
|
||||
to: "apps/api/src/routes/admin.ts PATCH /members/:id"
|
||||
via: "fetch PATCH /api/admin/members/:id"
|
||||
pattern: "api/admin/members/"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<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>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Add AdminMember.isAdmin + updateMemberProfile fetcher</name>
|
||||
<files>apps/pwa/src/api/client.ts</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `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.
|
||||
</behavior>
|
||||
<action>
|
||||
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<void>`. 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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.
|
||||
</acceptance_criteria>
|
||||
<done>AdminMember.isAdmin + updateMemberProfile exist, typed, and the PWA typechecks.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Pass PWA CI gates</name>
|
||||
<files>apps/pwa/src/api/client.ts</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
Run the PWA eslint + prettier gates on the modified file and fix any violations. Commit: `feat(20-02): add updateMemberProfile fetcher + AdminMember.isAdmin`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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.
|
||||
</acceptance_criteria>
|
||||
<done>PWA lint + format gates pass for client.ts; change committed.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
- `grep -n "updateMemberProfile" apps/pwa/src/api/client.ts` — fetcher present.
|
||||
- `AdminMember` has `isAdmin: boolean`.
|
||||
- PWA typecheck + eslint + prettier pass on client.ts.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/20-admin-member-editor-form-declutter/20-02-SUMMARY.md` when done.
|
||||
</output>
|
||||
Reference in New Issue
Block a user