Files
GearBox/.planning/milestones/v2.2-phases/28-profile-and-logto-integration/28-02-PLAN.md
Jean-Luc Makiola 2853477a75
All checks were successful
CI / ci (push) Successful in 1m15s
CI / e2e (push) Has been skipped
CI / deploy (push) Has been skipped
chore: archive v2.2 User Experience Polish milestone
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>
2026-04-13 16:00:35 +02:00

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
src/client/routes/profile.tsx
src/client/routes/settings.tsx
src/client/hooks/useAccount.ts
src/client/components/ProfileSection.tsx
true
truths artifacts key_links
/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
src/client/routes/profile.tsx
src/client/hooks/useAccount.ts
profile.tsx imports ProfileSection from components
profile.tsx imports useAccount hooks for password/email/deletion
settings.tsx no longer imports ProfileSection
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

<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>
Task 1: Create useAccount hooks for account management API calls src/client/hooks/useAccount.ts - 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) Create `src/client/hooks/useAccount.ts` with TanStack Query hooks:
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

Task 2: Create /profile page and remove ProfileSection from /settings src/client/routes/profile.tsx, src/client/routes/settings.tsx, src/client/components/ProfileSection.tsx - 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) **Create `src/client/routes/profile.tsx`:**

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.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. <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

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)

<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>