Phases 28-31 archived to milestones/v2.2-phases/ Requirements and roadmap snapshots archived to milestones/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
223 lines
10 KiB
Markdown
223 lines
10 KiB
Markdown
---
|
|
phase: 28-profile-and-logto-integration
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/client/routes/profile.tsx
|
|
- src/client/routes/settings.tsx
|
|
- src/client/hooks/useAccount.ts
|
|
- src/client/components/ProfileSection.tsx
|
|
autonomous: true
|
|
requirements: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- /profile route renders profile info, account info, security, and danger zone sections
|
|
- /settings no longer contains ProfileSection
|
|
- Settings page keeps weight unit, currency, import/export, and API keys only
|
|
- Profile page shows email from auth session and member-since date
|
|
- ProfileSection component is reused on the /profile page
|
|
artifacts:
|
|
- src/client/routes/profile.tsx
|
|
- src/client/hooks/useAccount.ts
|
|
key_links:
|
|
- profile.tsx imports ProfileSection from components
|
|
- profile.tsx imports useAccount hooks for password/email/deletion
|
|
- settings.tsx no longer imports ProfileSection
|
|
---
|
|
|
|
<objective>
|
|
Create dedicated /profile page with account management UI and separate it from /settings per D-01, D-02, D-03.
|
|
|
|
Purpose: Profile becomes its own page showing identity info and account actions. Settings keeps only app preferences (D-01). Profile shows displayName, bio, avatar, email, and member-since (D-02). No gear stats on profile (D-03).
|
|
Output: profile.tsx route, useAccount hooks, updated settings.tsx
|
|
</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/28-profile-and-logto-integration/28-CONTEXT.md
|
|
@.planning/phases/28-profile-and-logto-integration/28-UI-SPEC.md
|
|
|
|
@src/client/routes/settings.tsx
|
|
@src/client/components/ProfileSection.tsx
|
|
@src/client/hooks/useAuth.ts
|
|
@src/client/hooks/useProfile.ts
|
|
@src/client/lib/api.ts
|
|
</context>
|
|
|
|
<threat_model>
|
|
## Threat Model
|
|
|
|
| ID | Threat | Severity | Mitigation |
|
|
|----|--------|----------|------------|
|
|
| T-28-07 | Sensitive account actions accessible without auth | HIGH | Profile page only renders for authenticated users; redirect to /login if not authenticated |
|
|
| T-28-08 | Password visible in form state after submission | LOW | Clear password fields on successful submission; use type="password" inputs |
|
|
| T-28-09 | Account deletion without adequate confirmation | MEDIUM | Require typed "DELETE" string match before enabling delete button |
|
|
</threat_model>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create useAccount hooks for account management API calls</name>
|
|
<files>src/client/hooks/useAccount.ts</files>
|
|
<read_first>
|
|
- src/client/hooks/useAuth.ts (existing hook patterns — useQuery, useMutation, apiGet/apiPost)
|
|
- src/client/lib/api.ts (apiGet, apiPost, apiPut, apiDelete functions)
|
|
- src/shared/schemas.ts (schema shapes for request bodies)
|
|
</read_first>
|
|
<action>
|
|
Create `src/client/hooks/useAccount.ts` with TanStack Query hooks:
|
|
|
|
```typescript
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { apiGet, apiPost } from "../lib/api";
|
|
|
|
export function useHasPassword() {
|
|
return useQuery({
|
|
queryKey: ["account", "hasPassword"],
|
|
queryFn: () => apiGet<{ hasPassword: boolean }>("/api/account/has-password"),
|
|
});
|
|
}
|
|
|
|
export function useChangePassword() {
|
|
return useMutation({
|
|
mutationFn: (data: { currentPassword: string; newPassword: string }) =>
|
|
apiPost<{ ok: boolean }>("/api/account/password", data),
|
|
});
|
|
}
|
|
|
|
export function useChangeEmail() {
|
|
return useMutation({
|
|
mutationFn: (data: { newEmail: string }) =>
|
|
apiPost<{ ok: boolean }>("/api/account/email", data),
|
|
});
|
|
}
|
|
|
|
export function useDeleteAccount() {
|
|
return useMutation({
|
|
mutationFn: () =>
|
|
apiPost<{ ok: boolean; redirectTo: string }>("/api/account/delete", { confirmation: "DELETE" }),
|
|
});
|
|
}
|
|
```
|
|
|
|
Follow exact pattern from useAuth.ts — import from same api.ts, use same apiGet/apiPost functions. No queryClient invalidation needed since these are one-time actions (password change shows success message, deletion redirects).
|
|
</action>
|
|
<acceptance_criteria>
|
|
- src/client/hooks/useAccount.ts contains `useHasPassword`
|
|
- src/client/hooks/useAccount.ts contains `useChangePassword`
|
|
- src/client/hooks/useAccount.ts contains `useChangeEmail`
|
|
- src/client/hooks/useAccount.ts contains `useDeleteAccount`
|
|
- src/client/hooks/useAccount.ts imports from `../lib/api`
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>grep -q "useChangePassword" src/client/hooks/useAccount.ts && grep -q "useDeleteAccount" src/client/hooks/useAccount.ts</automated>
|
|
</verify>
|
|
<done>All four account management hooks exist, follow existing hook patterns, call correct API endpoints</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create /profile page and remove ProfileSection from /settings</name>
|
|
<files>src/client/routes/profile.tsx, src/client/routes/settings.tsx, src/client/components/ProfileSection.tsx</files>
|
|
<read_first>
|
|
- src/client/routes/settings.tsx (current layout — copy page structure pattern)
|
|
- src/client/components/ProfileSection.tsx (existing profile form to reuse)
|
|
- src/client/hooks/useAuth.ts (useAuth hook for email and auth state)
|
|
- src/client/hooks/useAccount.ts (hooks just created in Task 1)
|
|
- .planning/phases/28-profile-and-logto-integration/28-UI-SPEC.md (visual specs)
|
|
</read_first>
|
|
<action>
|
|
**Create `src/client/routes/profile.tsx`:**
|
|
|
|
TanStack Router file-based route at `/profile`. Structure per UI-SPEC.md:
|
|
|
|
```typescript
|
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
|
```
|
|
|
|
Page layout: `max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-6` (matches settings.tsx exactly).
|
|
|
|
Header: Back link (`← Back` to `/`) + `h1` "Profile" (`text-xl font-semibold text-gray-900`).
|
|
|
|
Four card sections, each in `bg-white rounded-xl border border-gray-100 p-5 space-y-6 mt-4`:
|
|
|
|
**Section 1: Profile Info** — Render existing `<ProfileSection />` component inside the first card. No changes to ProfileSection itself.
|
|
|
|
**Section 2: Account Info** — Read-only display:
|
|
- Email row: label "Email" + value from `auth?.user?.email` + "Change" button (triggers email change dialog state)
|
|
- Member since row: label "Member since" + formatted `users.createdAt` date
|
|
- Format date using `new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" })`.
|
|
- For email, show "No email on file" if `auth?.user?.email` is falsy.
|
|
- Email change inline form (shown when "Change" clicked): new email input + "Update Email" button. Uses `useChangeEmail()` hook. Show success/error message. Reset form on success.
|
|
|
|
**Section 3: Security** — Password management:
|
|
- Use `useHasPassword()` to check if user has a password.
|
|
- If has password: show 3 fields (current password, new password, confirm password).
|
|
- If no password: show 2 fields (new password, confirm password) with heading "Set Password".
|
|
- Password validation hint: `text-xs text-gray-400` — "Password must be at least 8 characters with uppercase, lowercase, and a number."
|
|
- Client-side validation: min 8 chars, at least one uppercase, one lowercase, one number. Disable submit until valid + passwords match.
|
|
- Uses `useChangePassword()` hook. On success: show green "Password updated" message, clear all fields (per T-28-08).
|
|
- On error (wrong current password): show red "Current password is incorrect" message.
|
|
|
|
**Section 4: Danger Zone** — Account deletion:
|
|
- Card uses `border-red-200` instead of `border-gray-100`.
|
|
- Description text per UI-SPEC: "Delete your account and all personal data. Public setups will be attributed to \"Deleted User\"."
|
|
- "Delete Account" button: `text-white bg-red-600 hover:bg-red-700 rounded-lg`.
|
|
- Clicking opens confirmation state (inline, not modal): warning text + input `placeholder="Type DELETE to confirm"` + disabled delete button (enabled when input === "DELETE").
|
|
- Uses `useDeleteAccount()` hook. On success: `window.location.href = "/logout"`.
|
|
|
|
**Auth guard:** If `!auth?.authenticated`, redirect to `/login` using `navigate({ to: "/login" })` in useEffect or render a redirect. Profile page is auth-only.
|
|
|
|
**Update `src/client/routes/settings.tsx`:**
|
|
- Remove the `{auth?.user && (<div>...<ProfileSection />...</div>)}` block entirely
|
|
- Keep: weight unit, currency, import/export, API keys sections
|
|
- Settings page no longer imports ProfileSection
|
|
|
|
**No changes to `src/client/components/ProfileSection.tsx`** — it stays as-is, just imported by profile.tsx instead of settings.tsx.
|
|
</action>
|
|
<acceptance_criteria>
|
|
- src/client/routes/profile.tsx contains `createFileRoute("/profile")`
|
|
- src/client/routes/profile.tsx contains `ProfileSection`
|
|
- src/client/routes/profile.tsx contains `useChangePassword`
|
|
- src/client/routes/profile.tsx contains `useDeleteAccount`
|
|
- src/client/routes/profile.tsx contains `"DELETE"` (confirmation string)
|
|
- src/client/routes/profile.tsx contains `border-red-200` (danger zone styling)
|
|
- src/client/routes/profile.tsx contains `Intl.DateTimeFormat` (member since formatting)
|
|
- src/client/routes/settings.tsx does NOT contain `ProfileSection`
|
|
- src/client/routes/settings.tsx does NOT contain `import.*ProfileSection`
|
|
- grep -c "ProfileSection" src/client/routes/settings.tsx returns 0
|
|
</acceptance_criteria>
|
|
<verify>
|
|
<automated>grep -q "createFileRoute" src/client/routes/profile.tsx && grep -q "useDeleteAccount" src/client/routes/profile.tsx && ! grep -q "ProfileSection" src/client/routes/settings.tsx</automated>
|
|
</verify>
|
|
<done>Profile page renders all four sections per UI-SPEC, settings page has no profile section, auth guard redirects unauthenticated users</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. `bun run lint` — no lint errors
|
|
2. Profile route file exists at correct path
|
|
3. Settings no longer contains ProfileSection
|
|
4. Profile page contains all four sections (profile, account, security, danger zone)
|
|
5. `bun run build` — build succeeds (TanStack Router auto-registers new route)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- /profile page exists with profile info, account info (email + member since), security (password change), and danger zone (account deletion)
|
|
- /settings page only contains weight unit, currency, import/export, and API keys
|
|
- ProfileSection component is reused on /profile page without modifications
|
|
- Password change shows different UIs for users with/without existing password
|
|
- Account deletion requires typed "DELETE" confirmation
|
|
- Email change shows inline form with success/error feedback
|
|
</success_criteria>
|