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>
10 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 | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 28-profile-and-logto-integration | 02 | execute | 1 |
|
true |
|
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
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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
<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> |
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).
<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>
grep -q "useChangePassword" src/client/hooks/useAccount.ts && grep -q "useDeleteAccount" src/client/hooks/useAccount.ts
All four account management hooks exist, follow existing hook patterns, call correct API endpoints
TanStack Router file-based route at /profile. Structure per UI-SPEC.md:
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.createdAtdate - Format date using
new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }). - For email, show "No email on file" if
auth?.user?.emailis 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-200instead ofborder-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.
<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>
grep -q "createFileRoute" src/client/routes/profile.tsx && grep -q "useDeleteAccount" src/client/routes/profile.tsx && ! grep -q "ProfileSection" src/client/routes/settings.tsx
Profile page renders all four sections per UI-SPEC, settings page has no profile section, auth guard redirects unauthenticated users
<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>