Files
GearBox/.planning/milestones/v2.2-phases/28-profile-and-logto-integration/28-RESEARCH.md
Jean-Luc Makiola 2853477a75
All checks were successful
CI / ci (push) Successful in 1m15s
CI / e2e (push) Has been skipped
CI / deploy (push) Has been skipped
chore: archive v2.2 User Experience Polish milestone
Phases 28-31 archived to milestones/v2.2-phases/
Requirements and roadmap snapshots archived to milestones/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:00:35 +02:00

13 KiB

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 firstPOST /api/users/{logtoSub}/password/verify with { password: currentPassword }. Returns 204 on success, 422 if wrong.
  2. Set new passwordPATCH /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

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

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