# 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`