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>
303 lines
13 KiB
Markdown
303 lines
13 KiB
Markdown
# 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
|