diff --git a/.planning/phases/28-profile-and-logto-integration/28-01-PLAN.md b/.planning/phases/28-profile-and-logto-integration/28-01-PLAN.md new file mode 100644 index 0000000..ede66ed --- /dev/null +++ b/.planning/phases/28-profile-and-logto-integration/28-01-PLAN.md @@ -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 +--- + + +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 + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + +## 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 | + + + + + + Task 1: Create Logto Management API client service + src/server/services/logto.service.ts, tests/services/logto.service.test.ts + + - 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) + + + - 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 + + +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. + + + - 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 + + + bun test tests/services/logto.service.test.ts + + LogtoManagementClient passes all unit tests with mocked fetch, token caching works, all CRUD methods call correct Logto API endpoints + + + + Task 2: Create account management API routes and register them + src/server/routes/account.ts, src/server/index.ts, src/shared/schemas.ts, src/shared/types.ts + + - 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) + + +**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). + + + - 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"` + + + bun run lint && grep -q "accountRoutes" src/server/index.ts && grep -q "changePasswordSchema" src/shared/schemas.ts + + Account management routes registered, all endpoints use requireAuth, password change verifies current password first, account deletion handles data anonymization + + + + + +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 + + + +- 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 + diff --git a/.planning/phases/28-profile-and-logto-integration/28-02-PLAN.md b/.planning/phases/28-profile-and-logto-integration/28-02-PLAN.md new file mode 100644 index 0000000..f64aa7c --- /dev/null +++ b/.planning/phases/28-profile-and-logto-integration/28-02-PLAN.md @@ -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 +--- + + +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 + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + +| 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 | + + + + + + 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: + +```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). + + + - 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` + + + 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: + +```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 `` 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 && (
......
)}` 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. +
+ + - 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 + + + 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) + + + +- /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 + diff --git a/.planning/phases/28-profile-and-logto-integration/28-03-PLAN.md b/.planning/phases/28-profile-and-logto-integration/28-03-PLAN.md new file mode 100644 index 0000000..26dcfdf --- /dev/null +++ b/.planning/phases/28-profile-and-logto-integration/28-03-PLAN.md @@ -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 +--- + + +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 + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + +## 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) | + + + + + + Task 1: Add profile navigation link and extend /me endpoint + src/client/routes/__root.tsx, src/server/routes/auth.ts, src/client/hooks/useAuth.ts + + - 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) + + +**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 +Profile +``` + +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). + + + - 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 + + + grep -q "createdAt" src/server/routes/auth.ts && grep -q "createdAt" src/client/hooks/useAuth.ts && grep -q "/profile" src/client/routes/__root.tsx + + /me returns createdAt, AuthState type includes it, navigation has profile link visible to authenticated users + + + + Task 2: Configure Logto branding, social connectors, and security policies + NONE (Logto Console configuration only) + + - .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) + + +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 + + + - 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 + + + echo "Manual verification required — Logto Console configuration" + + 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 + + + + + +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 + + + +- 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) +