docs(28): research Logto Management API integration for profile and account management
This commit is contained in:
302
.planning/phases/28-profile-and-logto-integration/28-RESEARCH.md
Normal file
302
.planning/phases/28-profile-and-logto-integration/28-RESEARCH.md
Normal 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
|
||||
Reference in New Issue
Block a user