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>
3.9 KiB
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+Inputfor username and password- Amber
Button(primary variant, full-width) Alertfor 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:
- Nobody hits the sign-in page until redirected from
/platform/ - By that time, SaaS has started and copied the dist files
- 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-expfor 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