feat: custom Logto sign-in UI with Cameleer branding
All checks were successful
CI / build (push) Successful in 40s
CI / docker (push) Successful in 50s

Replace Logto's default sign-in page with a custom React SPA that
matches the cameleer3-server login page using @cameleer/design-system.

- New Vite+React app at ui/sign-in/ with Experience API integration
- 4-step auth flow: init → verify password → identify → submit
- Design-system components: Card, Input, Button, FormField, Alert
- Same witty random subtitles as cameleer3-server LoginPage
- Dockerfile: add sign-in-frontend build stage, copy dist to image
- docker-compose: CUSTOM_UI_PATH on Logto, shared signinui volume
- SaaS entrypoint copies sign-in dist to shared volume on startup
- Add .gitattributes for LF line endings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-06 11:43:22 +02:00
parent b1c2832245
commit df220bc5f3
17 changed files with 2352 additions and 1 deletions

View File

@@ -0,0 +1,103 @@
# Custom Logto Sign-In UI
## Problem
Logto's default sign-in page uses Logto branding. While we configured colors and logos via `PATCH /api/sign-in-exp`, control over layout, typography, and components is limited. The sign-in experience is visually inconsistent with the cameleer3-server login page.
## Goal
Replace Logto's sign-in UI with a custom React SPA that visually matches the cameleer3-server login page, using `@cameleer/design-system` components for consistency across all deployment models.
## Scope
**MVP**: Username/password sign-in only.
**Later**: Sign-up, forgot password, social login, MFA.
## Architecture
### Source
Separate Vite+React app at `ui/sign-in/`. Own `package.json`, shares `@cameleer/design-system` v0.1.31.
### Build
Added as a Docker build stage in the SaaS `Dockerfile`. The dist is copied into the SaaS image at `/app/sign-in-dist/`.
### Deploy
SaaS entrypoint (`docker/saas-entrypoint.sh`) copies the built sign-in dist to a shared Docker volume (`signinui`). Logto reads from that volume via `CUSTOM_UI_PATH=/etc/logto/custom-ui`.
### Flow
```
User visits /platform/ → SaaS redirects to Logto OIDC
→ Logto sets interaction cookie, serves custom sign-in UI
→ User enters credentials → Experience API 4-step flow
→ Logto redirects back to /platform/callback with auth code
```
## Experience API
The custom UI communicates with Logto via the Experience API (stateful, cookie-based):
```
1. PUT /api/experience → 204 (init SignIn)
2. POST /api/experience/verification/password → 200 { verificationId }
3. POST /api/experience/identification → 204 (confirm identity)
4. POST /api/experience/submit → 200 { redirectTo }
```
The interaction cookie is set by `/oidc/auth` before the user lands on the sign-in page. All API calls use `credentials: "same-origin"`.
## Visual Design
Matches cameleer3-server LoginPage exactly:
- Centered `Card` (400px max-width, 32px padding)
- Logo: favicon.svg + "cameleer3" text (24px bold)
- Random witty subtitle (13px muted)
- `FormField` + `Input` for username and password
- Amber `Button` (primary variant, full-width)
- `Alert` for errors
- Background: `var(--bg-base)` (light beige)
- Dark mode support via design-system tokens
## Files
### New
| File | Purpose |
|------|---------|
| `ui/sign-in/package.json` | React 19 + @cameleer/design-system + Vite 6 |
| `ui/sign-in/.npmrc` | Gitea npm registry for @cameleer scope |
| `ui/sign-in/tsconfig.json` | TypeScript config |
| `ui/sign-in/vite.config.ts` | Vite build config |
| `ui/sign-in/index.html` | Entry HTML |
| `ui/sign-in/src/main.tsx` | React mount + design-system CSS import |
| `ui/sign-in/src/App.tsx` | App shell |
| `ui/sign-in/src/SignInPage.tsx` | Login form with Experience API integration |
| `ui/sign-in/src/SignInPage.module.css` | CSS Modules (matches server LoginPage) |
| `ui/sign-in/src/experience-api.ts` | Typed Experience API client |
| `docker/saas-entrypoint.sh` | Copies sign-in dist to shared volume |
| `.gitattributes` | LF line endings |
### Modified
| File | Change |
|------|--------|
| `Dockerfile` | Add `sign-in-frontend` build stage, copy dist to runtime image |
| `docker-compose.yml` | Add `CUSTOM_UI_PATH` to Logto, `signinui` shared volume |
## Docker Timing
Logto starts before SaaS (dependency chain: postgres → logto → logto-bootstrap → cameleer-saas). The `CUSTOM_UI_PATH` volume is initially empty. This is acceptable because:
1. Nobody hits the sign-in page until redirected from `/platform/`
2. By that time, SaaS has started and copied the dist files
3. Logto serves static files at request time, not startup time
## Future Extensions
- Add React Router for multiple pages (sign-up, forgot password)
- Fetch `GET /api/.well-known/sign-in-exp` for dynamic sign-in method detection
- Social login buttons via `POST /api/experience/verification/social`
- MFA via `POST /api/experience/verification/totp`
- Consent page via `GET /api/interaction/consent`