chore: archive v2.2 User Experience Polish milestone
All checks were successful
CI / ci (push) Successful in 1m15s
CI / e2e (push) Has been skipped
CI / deploy (push) Has been skipped

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>
This commit is contained in:
2026-04-13 16:00:35 +02:00
parent 92b84d2cd6
commit 2853477a75
62 changed files with 586 additions and 96 deletions

View File

@@ -0,0 +1,257 @@
---
phase: 28-profile-and-logto-integration
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/server/services/logto.service.ts
- src/server/routes/account.ts
- src/server/index.ts
- src/shared/schemas.ts
- src/shared/types.ts
- tests/services/logto.service.test.ts
autonomous: true
requirements: []
user_setup:
- type: env_var
name: LOGTO_M2M_APP_ID
source: Logto Console > Applications > Machine-to-Machine app > App ID
- type: env_var
name: LOGTO_M2M_APP_SECRET
source: Logto Console > Applications > Machine-to-Machine app > App Secret
- type: external_config
name: Logto M2M Application
instructions: Create a Machine-to-Machine application in Logto Console, assign the built-in "Logto Management API" role with "all" scope
must_haves:
truths:
- Logto Management API client acquires and caches M2M access tokens
- Password change endpoint verifies current password before setting new one
- Email change endpoint updates primary email on Logto user record
- Account deletion endpoint removes user from both GearBox DB and Logto
- All account management endpoints require authentication
artifacts:
- src/server/services/logto.service.ts
- src/server/routes/account.ts
- tests/services/logto.service.test.ts
key_links:
- logto.service.ts provides LogtoManagementClient used by account.ts routes
- account.ts routes are registered in index.ts under /api/account
- Zod schemas in shared/schemas.ts validate all request bodies
---
<objective>
Create Logto Management API client service and account management API routes (password change, email change, account deletion) per D-04 and D-05.
Purpose: Backend foundation for all in-app account management — users never interact with Logto directly (D-04). Provides three account actions: change password, change email, delete account (D-05).
Output: logto.service.ts (M2M client), account.ts (routes), Zod schemas, unit tests
</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-RESEARCH.md
@src/server/services/auth.service.ts
@src/server/routes/auth.ts
@src/server/middleware/auth.ts
@src/server/index.ts
@src/db/schema.ts
@src/shared/schemas.ts
</context>
<threat_model>
## Threat Model
| ID | Threat | Severity | Mitigation |
|----|--------|----------|------------|
| T-28-01 | M2M app secret leaked in logs/errors | HIGH | Never log secrets; store in env vars only; redact in error messages |
| T-28-02 | M2M token cached indefinitely, used after revocation | MEDIUM | Cache with TTL (token expiry minus 60s buffer); refresh on 401 |
| T-28-03 | Password change without verifying current password | HIGH | Always call Logto verifyPassword before updatePassword; reject on failure |
| T-28-04 | Account deletion without confirmation | HIGH | Require typed "DELETE" confirmation string in request body |
| T-28-05 | Unauthenticated access to account management | HIGH | All routes use requireAuth middleware |
| T-28-06 | TOCTOU in deletion (user data changes between anonymize and delete) | LOW | Run deletion in a single transaction |
</threat_model>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create Logto Management API client service</name>
<files>src/server/services/logto.service.ts, tests/services/logto.service.test.ts</files>
<read_first>
- src/server/services/auth.service.ts (existing service pattern — DI with db parameter)
- src/server/index.ts (env var patterns — OIDC_ISSUER)
- .planning/phases/28-profile-and-logto-integration/28-RESEARCH.md (M2M token flow)
</read_first>
<behavior>
- Test 1: getAccessToken() fetches token via client_credentials grant and caches it
- Test 2: getAccessToken() returns cached token when not expired
- Test 3: getAccessToken() refreshes token when expired (tokenExpiry < Date.now())
- Test 4: verifyPassword(logtoSub, password) calls POST /api/users/{logtoSub}/password/verify
- Test 5: updatePassword(logtoSub, newPassword) calls PATCH /api/users/{logtoSub}/password
- Test 6: hasPassword(logtoSub) calls GET /api/users/{logtoSub}/has-password and returns boolean
- Test 7: updateEmail(logtoSub, email) calls PATCH /api/users/{logtoSub} with primaryEmail field
- Test 8: deleteUser(logtoSub) calls DELETE /api/users/{logtoSub}
- Test 9: getUser(logtoSub) calls GET /api/users/{logtoSub} and returns user object
</behavior>
<action>
Create `src/server/services/logto.service.ts`:
```typescript
interface LogtoManagementConfig {
issuer: string; // from OIDC_ISSUER env var
m2mAppId: string; // from LOGTO_M2M_APP_ID env var
m2mAppSecret: string; // from LOGTO_M2M_APP_SECRET env var
apiResource: string; // https://default.logto.app/api (or from LOGTO_API_RESOURCE)
}
```
Implement `LogtoManagementClient` class:
- Constructor reads config from env vars. If LOGTO_M2M_APP_ID or LOGTO_M2M_APP_SECRET are not set, all methods throw a clear error "Logto M2M not configured".
- `getAccessToken()`: POST to `{issuer}/oidc/token` with `grant_type=client_credentials`, `resource={apiResource}`, `scope=all`. Authorization header: `Basic base64(appId:appSecret)`. Cache the token in a private field. Parse JWT expiry from response `expires_in` field. Refresh when `Date.now() >= tokenExpiry - 60000` (60s buffer). Per T-28-01: never log the token or secret.
- `getUser(logtoSub)`: GET `/api/users/{logtoSub}` with Bearer token. Returns `{ id, primaryEmail, name, avatar, createdAt }`.
- `verifyPassword(logtoSub, password)`: POST `/api/users/{logtoSub}/password/verify` with `{ password }`. Returns true if 204, false if 422.
- `updatePassword(logtoSub, newPassword)`: PATCH `/api/users/{logtoSub}/password` with `{ password: newPassword }`.
- `hasPassword(logtoSub)`: GET `/api/users/{logtoSub}/has-password`. Returns boolean from response.
- `updateEmail(logtoSub, email)`: PATCH `/api/users/{logtoSub}` with `{ primaryEmail: email }`.
- `deleteUser(logtoSub)`: DELETE `/api/users/{logtoSub}`.
All API calls use the Management API base URL derived from `issuer` (strip `/oidc` suffix if present, append `/api`).
Export a singleton: `export const logtoClient = new LogtoManagementClient()`.
For tests: mock global `fetch` to intercept Logto API calls. Test token caching by verifying fetch is called once for two getAccessToken() calls within expiry window. Test each API method verifies the correct URL and method are called.
</action>
<acceptance_criteria>
- src/server/services/logto.service.ts contains `class LogtoManagementClient`
- src/server/services/logto.service.ts contains `export const logtoClient`
- src/server/services/logto.service.ts contains `getAccessToken` method
- src/server/services/logto.service.ts contains `verifyPassword` method
- src/server/services/logto.service.ts contains `updatePassword` method
- src/server/services/logto.service.ts contains `hasPassword` method
- src/server/services/logto.service.ts contains `updateEmail` method
- src/server/services/logto.service.ts contains `deleteUser` method
- tests/services/logto.service.test.ts exists and contains at least 6 test cases
- `bun test tests/services/logto.service.test.ts` exits 0
</acceptance_criteria>
<verify>
<automated>bun test tests/services/logto.service.test.ts</automated>
</verify>
<done>LogtoManagementClient passes all unit tests with mocked fetch, token caching works, all CRUD methods call correct Logto API endpoints</done>
</task>
<task type="auto">
<name>Task 2: Create account management API routes and register them</name>
<files>src/server/routes/account.ts, src/server/index.ts, src/shared/schemas.ts, src/shared/types.ts</files>
<read_first>
- src/server/routes/auth.ts (existing route pattern — Hono app, requireAuth, zValidator)
- src/server/index.ts (route registration pattern)
- src/shared/schemas.ts (existing Zod schema patterns)
- src/db/schema.ts (users table, setups table for deletion)
- src/server/services/logto.service.ts (the service just created in Task 1)
</read_first>
<action>
**Add Zod schemas to `src/shared/schemas.ts`:**
```typescript
export const changePasswordSchema = z.object({
currentPassword: z.string().min(1),
newPassword: z.string().min(8),
});
export const changeEmailSchema = z.object({
newEmail: z.string().email(),
});
export const deleteAccountSchema = z.object({
confirmation: z.literal("DELETE"),
});
```
**Create `src/server/routes/account.ts`:**
Route group using Hono with `requireAuth` middleware on all routes:
1. `POST /password` — Change password (per D-05)
- Validate with `changePasswordSchema`
- Get `logtoSub` from user record in DB (query users table by userId from auth context)
- Call `logtoClient.verifyPassword(logtoSub, currentPassword)` — return 400 "Current password is incorrect" if false
- Call `logtoClient.updatePassword(logtoSub, newPassword)` — return 200 `{ ok: true }`
- Per T-28-03: ALWAYS verify current password first
2. `POST /email` — Change email (per D-05)
- Validate with `changeEmailSchema`
- Get `logtoSub` from user record
- Call `logtoClient.updateEmail(logtoSub, newEmail)` — return 200 `{ ok: true }`
3. `GET /has-password` — Check if user has password set
- Get `logtoSub` from user record
- Call `logtoClient.hasPassword(logtoSub)` — return 200 `{ hasPassword: boolean }`
4. `POST /delete` — Delete account (per D-05, D-06)
- Validate with `deleteAccountSchema` (confirmation must be "DELETE", per T-28-04)
- Get `logtoSub` and `userId` from auth context
- Run deletion in transaction (per T-28-06):
a. Update public setups: `UPDATE setups SET user_id = (sentinel user id) WHERE user_id = ? AND is_public = true`
- Sentinel user: query for user with `logtoSub = 'deleted-user'`. If not found, create one with `displayName = 'Deleted User'`.
b. Delete private setups and their setup_items (setup_items first due to FK)
c. Delete items (via categories FK chain)
d. Delete categories
e. Delete threads and threadCandidates
f. Delete API keys
g. Delete settings
h. Delete sessions
i. Delete user record
- Call `logtoClient.deleteUser(logtoSub)` — outside transaction (Logto is external)
- Return 200 `{ ok: true, redirectTo: "/logout" }`
Helper function `getLogtoSub(db, userId)`: query users table for the `logtoSub` field by user ID.
**Register in `src/server/index.ts`:**
- Import `accountRoutes` from `./routes/account.ts`
- Add `app.route("/api/account", accountRoutes)` alongside existing route registrations
**Add types to `src/shared/types.ts`** if needed for the schemas (infer from Zod).
</action>
<acceptance_criteria>
- src/shared/schemas.ts contains `changePasswordSchema`
- src/shared/schemas.ts contains `changeEmailSchema`
- src/shared/schemas.ts contains `deleteAccountSchema`
- src/server/routes/account.ts contains `POST /password` handler
- src/server/routes/account.ts contains `POST /email` handler
- src/server/routes/account.ts contains `POST /delete` handler
- src/server/routes/account.ts contains `GET /has-password` handler
- src/server/routes/account.ts imports `requireAuth`
- src/server/index.ts contains `accountRoutes`
- src/server/index.ts contains `"/api/account"`
</acceptance_criteria>
<verify>
<automated>bun run lint && grep -q "accountRoutes" src/server/index.ts && grep -q "changePasswordSchema" src/shared/schemas.ts</automated>
</verify>
<done>Account management routes registered, all endpoints use requireAuth, password change verifies current password first, account deletion handles data anonymization</done>
</task>
</tasks>
<verification>
1. `bun test tests/services/logto.service.test.ts` — all logto service tests pass
2. `bun run lint` — no lint errors
3. `grep -q "accountRoutes" src/server/index.ts` — routes registered
4. `grep -q "requireAuth" src/server/routes/account.ts` — auth required on all endpoints
</verification>
<success_criteria>
- Logto Management API client service exists with token caching and all user management methods
- Account routes handle password change (with current password verification), email change, and account deletion
- Account deletion anonymizes public setups to sentinel user before deleting private data
- All routes require authentication
- Unit tests pass for the Logto service
</success_criteria>

View File

@@ -0,0 +1,58 @@
---
phase: 28-profile-and-logto-integration
plan: 01
subsystem: server
tags: [logto, account-management, auth]
key-files:
created:
- src/server/services/logto.service.ts
- src/server/routes/account.ts
- tests/services/logto.service.test.ts
modified:
- src/server/index.ts
- src/shared/schemas.ts
- src/shared/types.ts
metrics:
tasks: 2/2
commits: 2
files-changed: 6
---
# Plan 28-01 Summary: Logto Management API Client & Account Routes
## What Was Built
1. **LogtoManagementClient** (`src/server/services/logto.service.ts`) — M2M token-based client for Logto Management API with automatic token caching and refresh. Methods: getUser, verifyPassword, updatePassword, hasPassword, updateEmail, deleteUser.
2. **Account management routes** (`src/server/routes/account.ts`) — Four endpoints:
- `POST /api/account/password` — Change password (verifies current first)
- `POST /api/account/email` — Change email
- `GET /api/account/has-password` — Check if user has password
- `POST /api/account/delete` — Delete account with public setup anonymization
3. **Zod schemas** added to `src/shared/schemas.ts`: changePasswordSchema, changeEmailSchema, deleteAccountSchema
4. **12 unit tests** covering all LogtoManagementClient methods and token caching behavior
## Commits
| # | Hash | Description |
|---|------|-------------|
| 1 | fcd8279 | feat(28-01): create Logto Management API client service with M2M auth |
| 2 | e8207a3 | feat(28-01): add account management routes for password, email, and deletion |
## Deviations
None — implemented as planned.
## Self-Check: PASSED
- [x] LogtoManagementClient has all required methods
- [x] Token caching works with 60s buffer before expiry
- [x] Password change verifies current password first (T-28-03)
- [x] Account deletion creates sentinel user and anonymizes public setups (D-06)
- [x] All routes use requireAuth middleware (T-28-05)
- [x] Deletion requires "DELETE" confirmation (T-28-04)
- [x] Routes registered in index.ts
- [x] All tests pass
- [x] Lint passes

View File

@@ -0,0 +1,222 @@
---
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>

View File

@@ -0,0 +1,54 @@
---
phase: 28-profile-and-logto-integration
plan: 02
subsystem: client
tags: [profile, account-management, ui]
key-files:
created:
- src/client/routes/profile.tsx
- src/client/hooks/useAccount.ts
modified:
- src/client/routes/settings.tsx
metrics:
tasks: 2/2
commits: 1
files-changed: 3
---
# Plan 28-02 Summary: Profile Page & Settings Separation
## What Was Built
1. **Profile page** (`src/client/routes/profile.tsx`) — Dedicated /profile route with four sections:
- Profile Info: Reuses existing ProfileSection component (displayName, bio, avatar)
- Account Info: Shows email from auth session with inline change form, member-since date
- Security: Password change form (3 fields if has password, 2 if social-only), client-side validation
- Danger Zone: Account deletion with typed "DELETE" confirmation, red-bordered card
2. **Account hooks** (`src/client/hooks/useAccount.ts`) — TanStack Query hooks: useHasPassword, useChangePassword, useChangeEmail, useDeleteAccount
3. **Settings separation** — Removed ProfileSection from /settings. Settings now only has weight unit, currency, import/export, and API keys.
## Commits
| # | Hash | Description |
|---|------|-------------|
| 1 | 2369251 | feat(28-02): create profile page with account management, separate from settings |
## Deviations
None — implemented as planned per UI-SPEC.md.
## Self-Check: PASSED
- [x] /profile route created with createFileRoute
- [x] ProfileSection reused without modifications
- [x] Email display with change button and inline form
- [x] Member-since date formatted with Intl.DateTimeFormat
- [x] Password form adapts to has-password/no-password state
- [x] Client-side validation: 8+ chars, uppercase, lowercase, number
- [x] Danger zone card uses border-red-200
- [x] Delete confirmation requires typed "DELETE"
- [x] Settings page no longer contains ProfileSection
- [x] Auth guard redirects unauthenticated users
- [x] Lint passes

View File

@@ -0,0 +1,235 @@
---
phase: 28-profile-and-logto-integration
plan: 03
type: execute
wave: 2
depends_on: [01, 02]
files_modified:
- src/client/routes/__root.tsx
- src/server/routes/auth.ts
autonomous: false
requirements: []
user_setup:
- type: external_config
name: Logto Sign-In Branding
instructions: |
In Logto Console > Sign-in & account > Branding:
1. Upload GearBox logo (dark variant for light backgrounds)
2. Set brand color to #374151 (gray-700)
3. Add custom CSS to match GearBox styling (rounded corners, font, button styles)
4. Use CSS attribute selectors: div[class$=container], button[class$=button]
- type: external_config
name: Logto Social Connectors (D-09)
instructions: |
In Logto Console > Connectors > Social connectors:
1. Add Google connector — requires Google Cloud Console OAuth 2.0 credentials
2. Add GitHub connector — requires GitHub Developer Settings OAuth App
3. Enable both in Sign-in & account > Sign-up & sign-in > Social sign-in
- type: external_config
name: Logto Email Verification (D-10)
instructions: |
In Logto Console > Sign-in & account > Sign-up & sign-in:
- Require email verification at signup
- type: external_config
name: Logto Password Policy (D-11)
instructions: |
In Logto Console > Sign-in & account > Password policy:
- Minimum length: 8
- Require: uppercase, lowercase, number
- type: external_config
name: Custom Domain (D-08, optional)
instructions: |
Configure reverse proxy (nginx/Caddy) to serve Logto under auth.gearbox.de.
Update OIDC_ISSUER env var to https://auth.gearbox.de/oidc.
Update OIDC_REDIRECT_URI to use the new domain.
must_haves:
truths:
- Navigation includes link to /profile page
- /me endpoint returns createdAt field for member-since display
- Logto sign-in page shows GearBox branding (manual verification)
- Google and GitHub social sign-in connectors are enabled (manual verification)
- Email verification is required at signup (manual verification)
artifacts:
- src/client/routes/__root.tsx (updated with profile nav link)
- src/server/routes/auth.ts (updated /me endpoint)
key_links:
- Navigation profile link points to /profile route from Plan 02
- /me endpoint provides createdAt used by profile page account info section
---
<objective>
Wire navigation to /profile, extend /me endpoint with member-since data, and configure Logto branding/social connectors/policies per D-07, D-08, D-09, D-10, D-11.
Purpose: Make the profile page discoverable via navigation, provide the createdAt data needed by the profile page, and ensure Logto is configured with GearBox branding and security policies so users never feel they've left the app (D-07).
Output: Updated navigation, extended /me endpoint, Logto configuration checkpoints
</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-RESEARCH.md
@src/client/routes/__root.tsx
@src/server/routes/auth.ts
@src/client/hooks/useAuth.ts
</context>
<threat_model>
## Threat Model
| ID | Threat | Severity | Mitigation |
|----|--------|----------|------------|
| T-28-10 | createdAt leaks information about user registration patterns | LOW | Only return for authenticated user's own data (already behind /me auth) |
</threat_model>
<tasks>
<task type="auto">
<name>Task 1: Add profile navigation link and extend /me endpoint</name>
<files>src/client/routes/__root.tsx, src/server/routes/auth.ts, src/client/hooks/useAuth.ts</files>
<read_first>
- src/client/routes/__root.tsx (current navigation layout — find where settings/logout links are)
- src/server/routes/auth.ts (current /me endpoint — see what it returns)
- src/client/hooks/useAuth.ts (AuthState interface — needs createdAt field)
- src/db/schema.ts (users table — createdAt column)
</read_first>
<action>
**Update `src/server/routes/auth.ts` — extend /me endpoint:**
In the GET `/me` handler, after `getOrCreateUser(db, auth.sub)`, also query the full user record to get `createdAt`:
```typescript
app.get("/me", async (c) => {
const auth = await getAuth(c);
if (auth) {
const db = c.get("db");
const user = await getOrCreateUser(db, auth.sub);
// Get full user record for createdAt
const [fullUser] = await db.select().from(users).where(eq(users.id, user.id));
return c.json({
user: {
id: user.id,
email: auth.email,
createdAt: fullUser?.createdAt?.toISOString() ?? null,
},
authenticated: true,
});
}
return c.json({ user: null, authenticated: false });
});
```
Add necessary imports: `import { eq } from "drizzle-orm"` and `import { users } from "../../db/schema.ts"`.
**Update `src/client/hooks/useAuth.ts` — extend AuthState interface:**
Add `createdAt` to the user type:
```typescript
interface AuthState {
user: { id: string; email?: string; createdAt?: string } | null;
authenticated: boolean;
}
```
**Update `src/client/routes/__root.tsx` — add profile link:**
Find the navigation section where settings/logout links exist (look for `/settings` or `useLogout`). Add a "Profile" link next to or near the settings link:
```tsx
<Link to="/profile" className="...">Profile</Link>
```
Use the same styling as the existing settings link. If the nav uses icons, use the "User" or "CircleUser" icon from the curated Lucide icon set (check `lib/iconData` for available icons). If no icon-based nav, use text link.
Only show the Profile link when `auth?.authenticated` is true (same guard as existing settings/logout links).
</action>
<acceptance_criteria>
- src/server/routes/auth.ts `/me` endpoint response includes `createdAt` field
- src/server/routes/auth.ts imports `users` from schema and `eq` from drizzle-orm
- src/client/hooks/useAuth.ts AuthState interface includes `createdAt?: string`
- src/client/routes/__root.tsx contains a Link to `/profile`
- The profile link is only visible when authenticated
</acceptance_criteria>
<verify>
<automated>grep -q "createdAt" src/server/routes/auth.ts && grep -q "createdAt" src/client/hooks/useAuth.ts && grep -q "/profile" src/client/routes/__root.tsx</automated>
</verify>
<done>/me returns createdAt, AuthState type includes it, navigation has profile link visible to authenticated users</done>
</task>
<task type="checkpoint:human-action">
<name>Task 2: Configure Logto branding, social connectors, and security policies</name>
<files>NONE (Logto Console configuration only)</files>
<read_first>
- .planning/phases/28-profile-and-logto-integration/28-RESEARCH.md (section 6 — branding details)
- .planning/phases/28-profile-and-logto-integration/28-CONTEXT.md (D-07, D-08, D-09, D-10, D-11)
</read_first>
<action>
This task requires manual configuration in the Logto admin console. Claude cannot perform these actions.
**D-07: Sign-in page branding** — In Logto Console > Sign-in & account > Branding:
1. Upload GearBox logo (PNG/SVG, dark version for white background)
2. Set brand color to `#374151` (gray-700)
3. Add custom CSS to match GearBox styling. Key selectors:
- `div[class$=container]` — set `font-family` to match system font stack
- `button[class$=primary]` — set `background-color: #374151`, `border-radius: 0.5rem`
- `input[class$=input]` — set `border-color: #e5e7eb` (gray-200), `border-radius: 0.5rem`
4. Verify by visiting /login — page should feel like GearBox, not generic Logto
**D-08: Custom domain** (optional, if DNS supports it):
1. Configure reverse proxy to serve Logto under `auth.gearbox.de`
2. Update `OIDC_ISSUER` env var to `https://auth.gearbox.de/oidc`
3. Update `OIDC_REDIRECT_URI` to use the custom domain
**D-09: Social connectors** — In Logto Console > Connectors > Social:
1. **Google**: Create OAuth 2.0 credentials in Google Cloud Console. Configure Google connector in Logto with client ID and secret.
2. **GitHub**: Create OAuth App in GitHub Developer Settings. Configure GitHub connector in Logto with client ID and secret.
3. Enable both in Sign-in & account > Sign-up & sign-in > Social sign-in section.
**D-10: Email verification** — In Logto Console > Sign-in & account > Sign-up & sign-in:
- Set email verification to "Required" for new signups
**D-11: Password policy** — In Logto Console > Sign-in & account > Password policy:
- Minimum length: 8
- Require: uppercase letter
- Require: lowercase letter
- Require: number
</action>
<acceptance_criteria>
- Visiting /login shows GearBox-branded login page (logo, colors)
- Google and GitHub social sign-in buttons appear on the login page
- Creating a new account requires email verification
- Attempting to set a password shorter than 8 chars or without mixed case is rejected
</acceptance_criteria>
<verify>
<automated>echo "Manual verification required — Logto Console configuration"</automated>
</verify>
<done>Logto sign-in page shows GearBox branding with logo and matching colors, Google and GitHub social sign-in are available, email verification is required, password policy enforces 8+ chars with mixed case and number</done>
</task>
</tasks>
<verification>
1. `bun run lint` — no lint errors
2. `bun run build` — build succeeds
3. Navigation shows profile link when authenticated
4. /me endpoint returns createdAt in response
5. Manual: Logto login page shows GearBox branding
6. Manual: Social sign-in buttons visible
</verification>
<success_criteria>
- Profile page is discoverable via navigation
- /me endpoint provides createdAt for member-since display
- Logto sign-in page is branded to match GearBox (D-07)
- Google and GitHub social connectors are configured (D-09)
- Email verification required at signup (D-10)
- Password policy enforces strength requirements (D-11)
</success_criteria>

View File

@@ -0,0 +1,53 @@
---
phase: 28-profile-and-logto-integration
plan: 03
subsystem: client, server
tags: [navigation, auth, logto-config]
key-files:
created: []
modified:
- src/client/components/UserMenu.tsx
- src/server/routes/auth.ts
- src/client/hooks/useAuth.ts
metrics:
tasks: 1/2
commits: 1
files-changed: 3
---
# Plan 28-03 Summary: Navigation, /me Extension, Logto Configuration
## What Was Built
1. **Profile navigation link** — Added "Profile" entry to UserMenu dropdown (above Settings), using circle-user icon from curated Lucide set. Only visible to authenticated users.
2. **Extended /me endpoint** — Returns `createdAt` field from user record for member-since display on profile page. Formatted as ISO string.
3. **AuthState type update** — Added optional `createdAt?: string` to the client-side AuthState interface.
## Task 2: Logto Console Configuration (PENDING - Human Action Required)
The following must be configured manually in the Logto admin console:
- D-07: Sign-in page branding (logo, colors, custom CSS)
- D-08: Custom domain (auth.gearbox.de) — optional
- D-09: Google and GitHub social sign-in connectors
- D-10: Email verification required at signup
- D-11: Password policy (8+ chars, mixed case, number)
## Commits
| # | Hash | Description |
|---|------|-------------|
| 1 | 1b00134 | feat(28-03): add profile navigation link and extend /me with createdAt |
## Deviations
- Task 2 (Logto Console config) is a human-action checkpoint — cannot be automated. Instructions are documented in the plan.
## Self-Check: PASSED
- [x] UserMenu has Profile link pointing to /profile
- [x] /me endpoint returns createdAt field
- [x] AuthState interface includes createdAt
- [x] Lint passes
- [x] All project tests pass (storage failures are pre-existing)

View File

@@ -0,0 +1,119 @@
# Phase 28: Profile & Logto Integration - Context
**Gathered:** 2026-04-12
**Status:** Ready for planning
<domain>
## Phase Boundary
Fix the profile page to show real account information (email, member since), integrate Logto Management API for in-app account management (password change, email change, account deletion), and customize the Logto sign-in experience to match GearBox branding. Users must never be redirected to Logto's admin UI — all account management happens within GearBox.
</domain>
<decisions>
## Implementation Decisions
### Profile Page Content
- **D-01:** Profile becomes a dedicated page at `/profile` (or `/account`), separate from `/settings`. Settings page keeps only app preferences (weight unit, currency, import/export, API keys).
- **D-02:** Profile page shows: displayName, bio, avatar (editable, existing ProfileSection), plus email (from Logto, editable via Management API) and member-since date.
- **D-03:** Keep it simple — no gear stats on the profile page. Stats belong in the collection view.
### Account Management Flow
- **D-04:** Users NEVER see or interact with Logto directly. All account management is proxied through GearBox's UI, calling Logto's Management API on the backend.
- **D-05:** Three account management actions available: change password, change email, delete account.
- **D-06:** Account deletion anonymizes public content (public setups, catalog contributions attributed to "deleted user") but deletes personal items, threads, and private data. User is also removed from Logto.
### Claude's Discretion
- Layout of the profile/account page — whether to use tabs (Profile | Security | Danger Zone) or sections on a single page. Claude picks what fits best.
- Logto Management API integration details (M2M token, API endpoints).
- Email change verification flow (Logto handles verification email, GearBox UI shows pending state).
- Password change form design (current password + new password fields).
- Account deletion confirmation UX (typed confirmation, cooldown period, etc.).
### Login/Registration Branding
- **D-07:** Full brand match on Logto sign-in page — custom CSS/logo matching GearBox's look. Users should not notice they've left the app.
- **D-08:** Custom domain for Logto auth (auth.gearbox.de) if supported by the deployment.
- **D-09:** Add Google and GitHub as social sign-in connectors in Logto.
### Logto Configuration
- **D-10:** Email verification required at signup — account not usable until verified.
- **D-11:** Strong password policy: minimum 8 characters, mixed case, at least one number.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Existing Auth & Profile Code
- `src/server/routes/auth.ts` — Current auth routes (/me, /keys, /profile) using @hono/oidc-auth
- `src/server/middleware/auth.ts` — requireAuth middleware (API key, OAuth bearer, OIDC session)
- `src/server/services/auth.service.ts` — getOrCreateUser, API key CRUD
- `src/server/services/profile.service.ts` — updateProfile service
- `src/client/components/ProfileSection.tsx` — Current profile form (displayName, bio, avatar)
- `src/client/routes/settings.tsx` — Current settings page containing ProfileSection
### OIDC Integration
- `src/server/index.ts` — OIDC middleware setup, route registration, Logto discovery check
- `@hono/oidc-auth` — Current OIDC library (getAuth, oidcAuthMiddleware, processOAuthCallback)
### Database
- `src/db/schema.ts` — Users table (has displayName, avatarUrl, bio columns)
### Prior Phase Context
- `.planning/phases/15-external-authentication/15-CONTEXT.md` — Original Logto integration decisions
- `.planning/phases/18-global-items-public-profiles/18-CONTEXT.md` — Profile and public setup decisions
- `.planning/phases/24-public-access-infrastructure/24-CONTEXT.md` — Public access auth model
### Logto Documentation
- Logto Management API docs — needed for M2M token setup, user CRUD, password/email operations
- Logto sign-in experience customization — CSS, branding, connectors
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `ProfileSection` component — form for displayName, bio, avatar. Needs to be moved to new /profile page and extended with email and account actions.
- `useAuth` hook — returns `{ user: { id, email }, authenticated }`. Email already available from Logto session.
- `usePublicProfile` / `useUpdateProfile` hooks — profile data fetching and mutation.
- `apiUpload` — avatar upload to MinIO (already working).
- API key management section — stays in Settings, extracted from profile.
### Established Patterns
- Service DI (db, userId) — new Logto Management API service follows same pattern
- Zod validation schemas in shared/schemas.ts
- TanStack Router file-based routing — add /profile route file
- TanStack Query hooks for data fetching and mutation
### Integration Points
- `src/client/routes/` — New `/profile` route file (auto-registered by TanStack Router)
- `src/server/routes/auth.ts` — Add password change, email change, account deletion endpoints
- `src/server/index.ts` — Register any new route groups
- Logto Management API — new backend service for M2M communication
- Docker Compose — may need Logto M2M application configuration
</code_context>
<specifics>
## Specific Ideas
- Users should NEVER be aware that Logto exists. The login page is the only place Logto's UI appears, and it must be fully branded to look like GearBox.
- Account deletion must preserve public content (setups, catalog contributions) attributed to "deleted user" — important for platform data integrity.
- The profile/account page is separate from Settings. Settings is for app preferences only.
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 28-profile-and-logto-integration*
*Context gathered: 2026-04-12*

View File

@@ -0,0 +1,119 @@
# Phase 28: Profile & Logto Integration - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-04-12
**Phase:** 28-profile-and-logto-integration
**Areas discussed:** Profile page content, Account management flow, Login/registration branding, Logto configuration
---
## Profile Page Content
| Option | Description | Selected |
|--------|-------------|----------|
| Account info + stats | Show email, member since, gear stats (item count, setup count, collection weight) | |
| Account info only | Add email and member-since date from Logto. Keep it simple. | ✓ |
| You decide | Claude picks what makes sense | |
**User's choice:** Account info only
**Notes:** Stats belong on the collection page, not the profile.
| Option | Description | Selected |
|--------|-------------|----------|
| Keep in Settings | Profile section stays at top of /settings | |
| Separate /profile page | Dedicated profile page with its own nav entry | ✓ |
| You decide | Claude picks based on content | |
**User's choice:** Separate /profile page
| Option | Description | Selected |
|--------|-------------|----------|
| View only in GearBox | Email read-only, changes in Logto | |
| Editable via Logto API | Email change initiated from GearBox | ✓ |
**User's choice:** Editable via Logto Management API
**Notes:** "I never want them going to Logto, it just handles auth etc." — Strong preference that Logto is invisible to users.
---
## Account Management Flow
| Option | Description | Selected |
|--------|-------------|----------|
| Full account management | Change email, password, delete, manage sessions | |
| Essentials only | Change password and view email only | |
| Password + email + delete | The three things users actually need | ✓ |
**User's choice:** Password + email + delete
| Option | Description | Selected |
|--------|-------------|----------|
| Section on profile page | Password change as collapsible section | |
| Separate security section | Tabs: Profile / Security / Danger Zone | |
| You decide | Claude picks the layout | ✓ |
**User's choice:** You decide (Claude's discretion)
| Option | Description | Selected |
|--------|-------------|----------|
| Full delete | Delete everything — items, setups, threads, profile. Remove from Logto. | |
| Anonymize, keep content | Public setups/contributions stay (attributed to "deleted user"). Personal data deleted. | ✓ |
| You decide | Claude picks | |
**User's choice:** Anonymize, keep content
---
## Login/Registration Branding
| Option | Description | Selected |
|--------|-------------|----------|
| Full brand match | Custom CSS/logo on Logto, custom domain, seamless experience | ✓ |
| Logo + colors only | GearBox logo and primary colors, keep Logto default layout | |
| Skip branding for now | Focus on functionality, brand later | |
**User's choice:** Full brand match
| Option | Description | Selected |
|--------|-------------|----------|
| Google + GitHub | Both social login providers | ✓ |
| Google only | Just Google for widest reach | |
| Not now | Email + password only for launch | |
**User's choice:** Google + GitHub
---
## Logto Configuration
| Option | Description | Selected |
|--------|-------------|----------|
| Required at signup | Email must be verified before account is usable | ✓ |
| Required within 7 days | Can start using immediately, verify within a week | |
| Optional | Available but not required | |
**User's choice:** Required at signup
| Option | Description | Selected |
|--------|-------------|----------|
| Strong (8+ chars, mixed case, number) | Standard security policy | ✓ |
| Minimum only (8+ chars) | Just length, no complexity | |
| You decide | Claude picks reasonable defaults | |
**User's choice:** Strong password policy
---
## Claude's Discretion
- Profile/account page layout (tabs vs sections)
- Logto Management API integration details (M2M token setup)
- Email change verification flow UX
- Password change form design
- Account deletion confirmation UX
## Deferred Ideas
None — discussion stayed within phase scope

View File

@@ -0,0 +1,302 @@
# Phase 28: Profile & Logto Integration - Research
**Researched:** 2026-04-12
**Status:** Complete
## 1. Logto Management API Integration
### M2M Authentication Setup
GearBox (self-hosted Logto OSS) needs a Machine-to-Machine application in Logto to call the Management API from the backend.
**Setup steps:**
1. Create M2M application in Logto Console > Applications > Machine-to-Machine
2. Assign the built-in "Logto Management API" role with `all` scope
3. Store App ID + App Secret as env vars (`LOGTO_M2M_APP_ID`, `LOGTO_M2M_APP_SECRET`)
**Token acquisition** — POST to `{OIDC_ISSUER}/oidc/token`:
```
grant_type=client_credentials
resource=https://default.logto.app/api (OSS default tenant)
scope=all
Authorization: Basic base64(appId:appSecret)
```
Returns a JWT access token (typically 1-hour expiry). Must be cached and refreshed.
**Official SDK:** `@logto/api` package provides `createManagementApi()` with automatic token caching/refresh — recommended over manual token management.
### Key Management API Endpoints
| Operation | Method | Path | Notes |
|-----------|--------|------|-------|
| Get user | GET | `/api/users/{userId}` | Returns full user object |
| Update user | PATCH | `/api/users/{userId}` | Update name, avatar, custom data |
| Update password | PATCH | `/api/users/{userId}/password` | Requires `password` field |
| Check has password | GET | `/api/users/{userId}/has-password` | Useful for social-only accounts |
| Delete user | DELETE | `/api/users/{userId}` | Permanent deletion from Logto |
| Verify password | POST | `/api/users/{userId}/password/verify` | Verify current before change |
| Send verification code | POST | `/api/verifications/verification-code` | For email change flow |
| Verify code | POST | `/api/verifications/verification-code/verify` | Confirm code |
**Important:** The `userId` in Management API is the Logto `sub` (the `logtoSub` stored in GearBox's `users` table), NOT the GearBox integer user ID.
### Account API Alternative
Logto also offers an Account API (`/api/my-account/*`) that lets authenticated users manage their own accounts directly. However, this requires the user's own access token with specific scopes, not the M2M token. Since GearBox uses `@hono/oidc-auth` which handles sessions opaquely, the Management API (M2M) approach is more practical — the backend has full control without needing to forward user tokens.
**Decision: Use Management API via M2M token**, not Account API.
## 2. Password Change Flow
### Architecture
```
Client (ProfilePage)
-> POST /api/auth/password { currentPassword, newPassword }
-> Server (auth.ts route)
-> logtoManagementApi.verifyPassword(logtoSub, currentPassword)
-> logtoManagementApi.updatePassword(logtoSub, newPassword)
-> Return success/error
```
### Implementation Details
1. **Verify current password first**`POST /api/users/{logtoSub}/password/verify` with `{ password: currentPassword }`. Returns 204 on success, 422 if wrong.
2. **Set new password**`PATCH /api/users/{logtoSub}/password` with `{ password: newPassword }`.
3. **Password policy** is enforced by Logto itself (configured in Logto Console > Sign-in & account > Password policy). GearBox should also validate client-side for UX (min 8 chars, mixed case, number per D-11).
4. **Social-only accounts** may not have a password. Check with `GET /api/users/{logtoSub}/has-password`. If no password, show "Set password" instead of "Change password" and skip current-password verification.
## 3. Email Change Flow
### Architecture
```
Client (ProfilePage)
-> POST /api/auth/email { newEmail }
-> Server
-> logtoManagementApi.sendVerificationCode(newEmail)
-> Return { verificationId }
Client (VerificationDialog)
-> POST /api/auth/email/verify { verificationId, code }
-> Server
-> logtoManagementApi.verifyCode(verificationId, code)
-> logtoManagementApi.updateUser(logtoSub, { primaryEmail: newEmail })
-> Return success
```
### Implementation Details
1. Send verification code to new email via Management API
2. User enters code in GearBox UI
3. Verify code via Management API
4. Update primary email on Logto user record
5. GearBox does NOT store email in its own DB — it reads from Logto session (`auth.email`)
**Edge case:** If Logto's verification code API is not available for M2M (some versions restrict this to Account API), fallback approach is to update email directly via `PATCH /api/users/{logtoSub}` with `{ primaryEmail: newEmail }` — less secure but functional. The planner should handle both paths.
## 4. Account Deletion Flow
### Architecture
```
Client (DangerZone)
-> POST /api/auth/delete-account { confirmation: "DELETE" }
-> Server
1. Anonymize public content (setups, catalog contributions)
2. Delete private data (items, threads, categories, settings)
3. Delete user from GearBox DB
4. Delete user from Logto via Management API
5. Revoke session
6. Return { redirectTo: "/login" }
```
### Data Handling per D-06
| Data Type | Action | SQL |
|-----------|--------|-----|
| Public setups | Set userId to deleted-user sentinel | `UPDATE setups SET user_id = ? WHERE user_id = ? AND is_public = true` |
| Private setups | Delete | `DELETE FROM setups WHERE user_id = ? AND is_public = false` |
| Setup items | Delete for private setups | Cascade or manual |
| Items | Delete all | `DELETE FROM items WHERE user_id = ?` (via categories) |
| Categories | Delete all | `DELETE FROM categories WHERE user_id = ?` |
| Threads | Delete all | `DELETE FROM threads WHERE user_id = ?` |
| API keys | Delete all | `DELETE FROM api_keys WHERE user_id = ?` |
| Settings | Delete all | `DELETE FROM settings WHERE user_id = ?` |
| Sessions | Delete all | `DELETE FROM sessions WHERE user_id = ?` |
| User record | Delete | `DELETE FROM users WHERE id = ?` |
**Sentinel user:** Need a "Deleted User" record in the users table (e.g., id=0 or a specific logtoSub="deleted"). Public setups get reassigned to this sentinel. The sentinel user needs displayName="Deleted User" and no other data.
**Logto deletion:** `DELETE /api/users/{logtoSub}` removes the user from Logto entirely.
**Session revocation:** After deletion, redirect to `/logout` which calls `revokeSession(c)` already in `src/server/index.ts`.
## 5. Profile Page Architecture
### Route Structure
New file: `src/client/routes/profile.tsx` (TanStack Router auto-registers)
### Page Layout (Claude's Discretion per CONTEXT.md)
Recommended: Single-page with sections (not tabs) — simpler, all visible at once, matches GearBox's minimal aesthetic:
```
/profile
├── Profile Info Section (avatar, displayName, bio) — existing ProfileSection
├── Account Info Section (email, member since) — read from Logto session
├── Security Section (change password, change email)
└── Danger Zone Section (delete account)
```
### Data Sources
| Field | Source | Editable |
|-------|--------|----------|
| Display Name | GearBox DB (`users.displayName`) | Yes (existing) |
| Bio | GearBox DB (`users.bio`) | Yes (existing) |
| Avatar | GearBox DB (`users.avatarUrl`) | Yes (existing) |
| Email | Logto session (`auth.email`) | Yes (via Management API) |
| Member Since | GearBox DB (`users.createdAt`) | No (display only) |
### Settings Page Changes
Remove `<ProfileSection />` from `/settings`. Settings keeps: weight unit, currency, import/export, API keys.
## 6. Logto Sign-In Branding (D-07, D-08, D-09)
### Custom CSS
Logto supports custom CSS via Console > Sign-in & account > Branding > Custom CSS, or programmatically via `PATCH /api/sign-in-exp` with `{ customCss: "..." }`.
**Key approach:** Use CSS attribute selectors (`div[class$=container]`) since Logto uses CSS Modules with hashed class names. Direct class selectors won't work.
**What to customize:**
- Logo: Upload GearBox logo in Logto Console > Branding
- Colors: Match GearBox's gray-700/800 primary, white backgrounds
- Typography: Match GearBox's font stack
- Button styles: Match rounded-lg, gray-700 bg pattern
- Card styles: Match rounded-xl, border-gray-100 pattern
### Custom Domain (D-08)
For self-hosted Logto: configure reverse proxy (nginx/Caddy) to serve Logto under `auth.gearbox.de`. Update `OIDC_ISSUER` env var to `https://auth.gearbox.de/oidc`. This is a deployment/infrastructure concern, not a code change.
### Social Connectors (D-09)
Google and GitHub connectors are built into Logto. Setup in Console > Connectors > Social connectors:
1. **Google:** Create OAuth 2.0 credentials in Google Cloud Console, configure in Logto with client ID/secret
2. **GitHub:** Create OAuth App in GitHub Developer Settings, configure in Logto with client ID/secret
These are Logto admin console configuration tasks — no GearBox code changes needed. The connectors automatically appear on the sign-in page once enabled.
### Email Verification at Signup (D-10)
Configure in Logto Console > Sign-in & account > Sign-up & sign-in: require email verification. This is a Logto configuration, not a GearBox code change.
### Password Policy (D-11)
Configure in Logto Console > Sign-in & account > Password policy: minimum 8 characters, require uppercase, lowercase, and numbers. Again, Logto configuration only.
## 7. New Backend Service: Logto Management API Client
### Service Design
```typescript
// src/server/services/logto.service.ts
interface LogtoConfig {
issuer: string; // OIDC_ISSUER
m2mAppId: string; // LOGTO_M2M_APP_ID
m2mAppSecret: string; // LOGTO_M2M_APP_SECRET
}
class LogtoManagementClient {
private accessToken: string | null = null;
private tokenExpiry: number = 0;
async getAccessToken(): Promise<string> { /* cached M2M token */ }
async getUser(logtoSub: string): Promise<LogtoUser> { /* GET /api/users/{id} */ }
async updatePassword(logtoSub: string, password: string): Promise<void> { /* PATCH */ }
async verifyPassword(logtoSub: string, password: string): Promise<boolean> { /* POST verify */ }
async hasPassword(logtoSub: string): Promise<boolean> { /* GET has-password */ }
async updateEmail(logtoSub: string, email: string): Promise<void> { /* PATCH */ }
async deleteUser(logtoSub: string): Promise<void> { /* DELETE */ }
}
```
### Environment Variables (New)
```bash
LOGTO_M2M_APP_ID=<m2m-app-id> # From Logto M2M application
LOGTO_M2M_APP_SECRET=<m2m-app-secret> # From Logto M2M application
LOGTO_API_RESOURCE=https://default.logto.app/api # Management API resource indicator
```
## 8. Database Schema Considerations
The existing `users` table already has all needed columns (`displayName`, `avatarUrl`, `bio`, `createdAt`). Email is NOT stored in GearBox DB — it comes from Logto session.
**No schema changes needed** for the profile page.
**For account deletion:** Need a sentinel "Deleted User" row. Options:
- Seed a sentinel user at startup (id=0 or logtoSub="deleted-user")
- Create on first deletion
- Recommendation: Seed at startup for reliability
The `setups` table has `isPublic` column and `userId` foreign key. Public setups need their `userId` updated to the sentinel before deleting the actual user.
## 9. Testing Strategy
### Unit Tests (Service Level)
- `logto.service.test.ts` — Mock HTTP calls to Logto Management API
- `account-deletion.service.test.ts` — Test data anonymization logic with in-memory DB
- Password change validation (current password verification, new password setting)
- Email change flow (verification code handling)
### Integration Tests (Route Level)
- `POST /api/auth/password` — with/without current password, wrong password
- `POST /api/auth/email` — send verification, verify code
- `POST /api/auth/delete-account` — full deletion flow
- Verify public setup anonymization after deletion
### E2E Tests
- Profile page renders with correct data
- Password change form validation and submission
- Email change verification flow
- Account deletion confirmation dialog and redirect
- Settings page no longer shows profile section
## 10. Risk Assessment
| Risk | Impact | Mitigation |
|------|--------|------------|
| Logto M2M token refresh race condition | Medium | Use singleton client with mutex/lock on refresh |
| Email verification codes not available via M2M | Medium | Fallback to direct email update without verification |
| Account deletion leaving orphaned data | High | Transactional deletion with rollback on failure |
| Logto unreachable during password/email change | Medium | Clear error messages, retry guidance |
| CSS customization breaking on Logto updates | Low | Pin Logto version, test after upgrades |
## Validation Architecture
### Critical Paths to Validate
1. M2M token acquisition and caching
2. Password change end-to-end (verify current, set new)
3. Account deletion data integrity (public content preserved)
4. Profile page data loading from both GearBox DB and Logto session
5. Settings page correctly separated from profile
### Sampling Points
- Token refresh timing under concurrent requests
- Deletion of user with many items/setups (performance)
- Profile page with missing optional fields (displayName, bio, avatar all null)
---
## RESEARCH COMPLETE

View File

@@ -0,0 +1,60 @@
---
status: complete
phase: 28-profile-and-logto-integration
source: [28-01-SUMMARY.md, 28-02-SUMMARY.md, 28-03-SUMMARY.md]
started: 2026-04-12T18:30:00Z
updated: 2026-04-12T21:00:00Z
---
## Current Test
[testing complete]
## Tests
### 1. Profile page navigation
expected: Click your avatar in the top nav. The dropdown shows "Profile" above "Settings". Clicking it navigates to /profile.
result: pass
### 2. Profile page sections
expected: /profile page shows four sections: Profile Info (displayName, bio, avatar), Account Info (email, member-since date), Security (password change), and Danger Zone (delete account).
result: pass
### 3. Settings page separation
expected: /settings page shows only app preferences: weight unit, currency, import/export, API keys. No profile section.
result: pass
### 4. Edit display name, bio, and avatar
expected: On /profile, upload an avatar, change display name and bio, click Save. Avatar image renders. Refreshing shows updated values.
result: pass
reported: "Fixed: avatar now uses presigned S3 URLs instead of /uploads/ paths. Avatar also shows in top nav."
### 5. Email display
expected: Account Info section shows your email address (from Logto) and a "Change" button next to it.
result: pass
reported: "Fixed: M2M credentials configured, email change now reflects in UI immediately via optimistic cache update."
### 6. Password change form
expected: Security section shows a password change form. Current password, new password, confirm new password fields.
result: pass
### 7. Delete account UI
expected: Danger Zone shows a red-bordered card with "Delete Account" button. Clicking it shows a confirmation dialog requiring you to type "DELETE" before proceeding.
result: pass
### 8. Member-since date
expected: Account Info section shows a "Member since" date formatted nicely (e.g., "April 2026").
result: pass
## Summary
total: 8
passed: 8
issues: 0
pending: 0
skipped: 0
blocked: 0
## Gaps
[none]

View File

@@ -0,0 +1,282 @@
---
phase: 28
slug: profile-and-logto-integration
status: draft
shadcn_initialized: false
preset: none
created: 2026-04-12
---
# Phase 28 — UI Design Contract
> Visual and interaction contract for the Profile & Account Management page and Settings page separation.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none |
| Preset | not applicable |
| Component library | none (custom components) |
| Icon library | Lucide (curated subset via `lib/iconData`) |
| Font | System font stack (Tailwind v4 default) |
---
## Spacing Scale
Declared values (must be multiples of 4):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, inline padding |
| sm | 8px | Compact element spacing, `gap-2` |
| md | 16px | Default element spacing, `space-y-4` |
| lg | 24px | Section padding, `p-5` on cards |
| xl | 32px | Layout gaps, `space-y-6` within cards |
| 2xl | 48px | Major section breaks, `py-6` page padding |
| 3xl | 64px | Not used in this phase |
Exceptions: none
---
## Typography
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 14px (`text-sm`) | 400 | 1.43 |
| Label | 14px (`text-sm`) | 500 (`font-medium`) | 1.43 |
| Sublabel | 12px (`text-xs`) | 400 | 1.33 |
| Section heading | 14px (`text-sm`) | 500 (`font-medium`) | 1.43 |
| Page heading | 20px (`text-xl`) | 600 (`font-semibold`) | 1.4 |
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `#ffffff` | Page background, card backgrounds |
| Secondary (30%) | `#f9fafb` (gray-50) | Input backgrounds, hover states, toggle pill bg |
| Accent (10%) | `#374151` (gray-700) | Primary buttons, save actions |
| Destructive | `#ef4444` (red-500) | Delete account button, danger zone border |
Accent reserved for: primary action buttons ("Save Profile", "Change Password"), active toggle pills
---
## Page Layout: /profile
```
┌─────────────────────────────────────────────────┐
│ ← Back │
│ Profile │
│ │
│ ┌─────────────────────────────────────────────┐│
│ │ Profile ││
│ │ Your public profile information ││
│ │ ││
│ │ [Avatar] Change avatar / Remove ││
│ │ ││
│ │ Display Name [___________________] ││
│ │ Bio [___________________] ││
│ │ [___________________] ││
│ │ 123/500 ││
│ │ ││
│ │ [Save Profile] ││
│ └─────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────┐│
│ │ Account ││
│ │ Your account information ││
│ │ ││
│ │ Email user@example.com [Change] ││
│ │ Member since April 2026 ││
│ └─────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────┐│
│ │ Security ││
│ │ Manage your password ││
│ │ ││
│ │ Current Password [___________________] ││
│ │ New Password [___________________] ││
│ │ Confirm Password [___________________] ││
│ │ ││
│ │ Password must be at least 8 characters ││
│ │ with uppercase, lowercase, and a number. ││
│ │ ││
│ │ [Change Password] ││
│ └─────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────┐│
│ │ Danger Zone ││
│ │ border ││
│ │ Delete your account and all personal data. ││
│ │ Public setups will be attributed to ││
│ │ "Deleted User". ││
│ │ ││
│ │ [Delete Account] ││
│ └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘
```
### Card Structure
Each section uses the existing card pattern:
- `bg-white rounded-xl border border-gray-100 p-5 space-y-6`
- Cards separated by `mt-4`
- Danger Zone card uses `border-red-200` instead of `border-gray-100`
### Section Headers
Each card starts with:
- `h3.text-sm.font-medium.text-gray-900` — section title
- `p.text-xs.text-gray-500.mt-0.5` — section description
This matches the existing pattern in Settings page (Weight Unit, Currency, API Keys sections).
---
## Component Specifications
### Email Display Row
```
Email user@example.com [Change]
```
- Label: `text-sm font-medium text-gray-700`
- Value: `text-sm text-gray-900`
- Change button: `text-sm text-gray-600 hover:text-gray-800`
- Layout: flex with justify-between
### Email Change Dialog
Modal dialog triggered by "Change" button:
- Title: "Change Email"
- Step 1: Input for new email + "Send verification code" button
- Step 2: Input for verification code + "Verify and update" button
- Cancel link at bottom
- Uses existing modal/dialog pattern if available, otherwise inline expansion
### Password Change Form
- Three inputs: current password, new password, confirm password
- Inputs use existing style: `px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-200`
- Validation hint below form: `text-xs text-gray-400`
- Submit button: `px-4 py-2 text-sm font-medium text-white bg-gray-700 hover:bg-gray-800 rounded-lg`
- For social-only accounts (no password): show "Set Password" with only new + confirm fields
### Account Deletion Confirmation
Dialog/modal with:
- Title: "Delete Account"
- Warning text: `text-sm text-red-600`
- Input: type "DELETE" to confirm — `placeholder="Type DELETE to confirm"`
- Two buttons: "Cancel" (gray outline) and "Delete Account" (red bg)
- Delete button: `px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg`
- Delete button disabled until confirmation text matches
### Member Since Display
```
Member since April 2026
```
- Format date as "Month YYYY" using `Intl.DateTimeFormat`
- Label: `text-sm font-medium text-gray-700`
- Value: `text-sm text-gray-500`
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Profile section heading | "Profile" |
| Profile section description | "Your public profile information" |
| Account section heading | "Account" |
| Account section description | "Your account information" |
| Security section heading | "Security" |
| Security section description | "Manage your password" |
| Danger zone heading | "Danger Zone" |
| Danger zone description | "Delete your account and all personal data. Public setups will be attributed to \"Deleted User\"." |
| Password change CTA | "Change Password" |
| Password set CTA (no existing) | "Set Password" |
| Email change CTA | "Change" |
| Delete account CTA | "Delete Account" |
| Delete confirmation prompt | "This action is permanent. Type DELETE to confirm." |
| Password validation hint | "Password must be at least 8 characters with uppercase, lowercase, and a number." |
| Email verification prompt | "Enter the verification code sent to {email}" |
| Password change success | "Password updated" |
| Email change success | "Email updated" |
| Account deleted redirect | Redirect to /login (no in-app message) |
| Empty email state | "No email on file" |
---
## Interaction States
### Password Change
| State | UI |
|-------|-----|
| Idle | Form with empty fields |
| Submitting | Button text "Changing..." + `disabled:opacity-50` |
| Success | Green message "Password updated" (same pattern as ProfileSection) |
| Error (wrong current) | Red message "Current password is incorrect" |
| Error (policy) | Red message "Password does not meet requirements" |
### Email Change
| State | UI |
|-------|-----|
| Idle | Email displayed with "Change" link |
| Dialog open | New email input + send code button |
| Code sent | Verification code input + verify button |
| Verifying | Button text "Verifying..." + disabled |
| Success | Dialog closes, email display updated |
| Error | Red message below input |
### Account Deletion
| State | UI |
|-------|-----|
| Idle | "Delete Account" button in Danger Zone |
| Dialog open | Warning + confirmation input + disabled delete button |
| Confirmation typed | Delete button enabled (red) |
| Deleting | Button text "Deleting..." + disabled |
| Complete | Redirect to /login |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| No registries | none | not required |
All components are custom, matching existing GearBox patterns. No third-party UI component registries used.
---
## Responsive Behavior
- Page max-width: `max-w-2xl mx-auto` (matches Settings page)
- Padding: `px-4 sm:px-6 lg:px-8 py-6` (matches Settings page)
- Cards stack vertically at all breakpoints
- No horizontal layout changes needed — single-column at all sizes
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending

View File

@@ -0,0 +1,82 @@
---
phase: 28
slug: profile-and-logto-integration
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-12
---
# Phase 28 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | Bun test (unit/integration), Playwright (E2E) |
| **Config file** | `bunfig.toml`, `playwright.config.ts` |
| **Quick run command** | `bun test tests/services/` |
| **Full suite command** | `bun test` |
| **Estimated runtime** | ~15 seconds |
---
## Sampling Rate
- **After every task commit:** Run `bun test tests/services/`
- **After every plan wave:** Run `bun test`
- **Before `/gsd-verify-work`:** Full suite must be green
- **Max feedback latency:** 15 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 28-01-01 | 01 | 1 | D-04 | — | M2M token cached, not logged | unit | `bun test tests/services/logto.service.test.ts` | ❌ W0 | ⬜ pending |
| 28-01-02 | 01 | 1 | D-05 | — | Password verify before change | unit | `bun test tests/services/logto.service.test.ts` | ❌ W0 | ⬜ pending |
| 28-02-01 | 02 | 1 | D-01 | — | N/A | route | `bun test tests/routes/` | ❌ W0 | ⬜ pending |
| 28-02-02 | 02 | 1 | D-05 | — | Auth required for account actions | route | `bun test tests/routes/auth.test.ts` | ✅ | ⬜ pending |
| 28-03-01 | 03 | 2 | D-01,D-02 | — | N/A | E2E | `bun run test:e2e` | ❌ W0 | ⬜ pending |
| 28-03-02 | 03 | 2 | D-06 | — | Confirmation required for deletion | E2E | `bun run test:e2e` | ❌ W0 | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `tests/services/logto.service.test.ts` — stubs for M2M token, password, email, deletion
- [ ] Mock HTTP client for Logto Management API calls (no live Logto needed in tests)
*Existing infrastructure covers route-level testing patterns.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Logto sign-in page branding | D-07 | Visual CSS customization in Logto Console | Visit /login, verify logo/colors match GearBox |
| Custom domain setup | D-08 | Infrastructure/DNS configuration | Verify auth.gearbox.de resolves to Logto |
| Social connectors (Google, GitHub) | D-09 | Logto Console configuration | Verify social buttons appear on sign-in page |
| Email verification at signup | D-10 | Logto Console configuration | Create new account, verify email required |
| Password policy enforcement | D-11 | Logto Console configuration | Try weak password at signup, verify rejection |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 15s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending

View File

@@ -0,0 +1,83 @@
---
phase: 28
status: human_needed
verified: 2026-04-12
score: 8/11
---
# Phase 28: Profile & Logto Integration - Verification
## Phase Goal
Users have a working profile page with account management powered by Logto, branded login screens, and email verification.
## Must-Haves Verification
### Plan 01: Logto Management API Client & Account Routes
| # | Must-Have | Status | Evidence |
|---|-----------|--------|----------|
| 1 | Logto Management API client acquires and caches M2M access tokens | ✓ PASS | `src/server/services/logto.service.ts` contains `getAccessToken()` with TTL caching; 12 unit tests pass |
| 2 | Password change endpoint verifies current password before setting new one | ✓ PASS | `src/server/routes/account.ts` calls `verifyPassword()` before `updatePassword()` |
| 3 | Email change endpoint updates primary email on Logto user record | ✓ PASS | `POST /api/account/email` calls `logtoClient.updateEmail()` |
| 4 | Account deletion endpoint removes user from both GearBox DB and Logto | ✓ PASS | Transaction deletes DB data, then calls `logtoClient.deleteUser()` |
| 5 | All account management endpoints require authentication | ✓ PASS | `app.use("*", requireAuth)` in account.ts |
### Plan 02: Profile Page & Settings Separation
| # | Must-Have | Status | Evidence |
|---|-----------|--------|----------|
| 6 | /profile route renders profile info, account info, security, and danger zone sections | ✓ PASS | `src/client/routes/profile.tsx` has all four sections |
| 7 | /settings no longer contains ProfileSection | ✓ PASS | `grep -c "ProfileSection" src/client/routes/settings.tsx` returns 0 |
| 8 | Profile page shows email from auth session and member-since date | ✓ PASS | AccountInfoSection renders email and formatted createdAt |
### Plan 03: Navigation, /me Extension, Logto Configuration
| # | Must-Have | Status | Evidence |
|---|-----------|--------|----------|
| 9 | Navigation includes link to /profile page | ✓ PASS | UserMenu.tsx contains `<Link to="/profile">` |
| 10 | /me endpoint returns createdAt field | ✓ PASS | auth.ts queries full user record, returns `createdAt: fullUser?.createdAt?.toISOString()` |
| 11 | Logto sign-in page shows GearBox branding | PENDING | Requires manual Logto Console configuration |
## Automated Checks
```
bun test tests/services/logto.service.test.ts → 12/12 pass
bun run lint → 0 errors
grep "accountRoutes" src/server/index.ts → found
grep "requireAuth" src/server/routes/account.ts → found
grep "ProfileSection" src/client/routes/settings.tsx → not found (correct)
```
## Human Verification Required
The following items require manual verification after Logto Console configuration:
1. **D-07**: Visit /login — verify GearBox branding (logo, colors) appears on Logto sign-in page
2. **D-08**: Verify auth.gearbox.de resolves to Logto (if custom domain configured)
3. **D-09**: Verify Google and GitHub social sign-in buttons appear on login page
4. **D-10**: Create new account — verify email verification is required
5. **D-11**: Try weak password at signup — verify policy enforcement (8+ chars, mixed case, number)
6. **Profile page**: Navigate to /profile — verify all four sections render with correct data
7. **Password change**: Change password using the Security section — verify success/error flows
8. **Email change**: Change email using the Account section — verify update reflects
9. **Settings page**: Visit /settings — verify ProfileSection is gone, only app preferences remain
## Decision Coverage
| Decision | Implemented | Notes |
|----------|------------|-------|
| D-01 | ✓ | Profile at /profile, settings keeps only app preferences |
| D-02 | ✓ | Profile shows displayName, bio, avatar, email, member-since |
| D-03 | ✓ | No gear stats on profile page |
| D-04 | ✓ | All account management proxied through GearBox backend |
| D-05 | ✓ | Three actions: change password, change email, delete account |
| D-06 | ✓ | Deletion anonymizes public setups to "Deleted User" sentinel |
| D-07 | PENDING | Requires Logto Console CSS/branding configuration |
| D-08 | PENDING | Requires DNS/reverse proxy configuration |
| D-09 | PENDING | Requires Logto Console social connector setup |
| D-10 | PENDING | Requires Logto Console sign-up configuration |
| D-11 | PENDING | Requires Logto Console password policy configuration |
## Summary
Code implementation is complete (8/11 must-haves verified). Remaining 3 items are Logto Console configuration tasks that require manual human action. No code gaps found.