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>
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:
- Create M2M application in Logto Console > Applications > Machine-to-Machine
- Assign the built-in "Logto Management API" role with
allscope - 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
- Verify current password first —
POST /api/users/{logtoSub}/password/verifywith{ password: currentPassword }. Returns 204 on success, 422 if wrong. - Set new password —
PATCH /api/users/{logtoSub}/passwordwith{ password: newPassword }. - 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).
- 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
- Send verification code to new email via Management API
- User enters code in GearBox UI
- Verify code via Management API
- Update primary email on Logto user record
- 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) |
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:
- Google: Create OAuth 2.0 credentials in Google Cloud Console, configure in Logto with client ID/secret
- 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 APIaccount-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 passwordPOST /api/auth/email— send verification, verify codePOST /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
- M2M token acquisition and caching
- Password change end-to-end (verify current, set new)
- Account deletion data integrity (public content preserved)
- Profile page data loading from both GearBox DB and Logto session
- 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)