feat: custom Logto sign-in UI with Cameleer branding
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:
63
ui/sign-in/src/experience-api.ts
Normal file
63
ui/sign-in/src/experience-api.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
const BASE = '/api/experience';
|
||||
|
||||
async function request(method: string, path: string, body?: unknown): Promise<Response> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method,
|
||||
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function initInteraction(): Promise<void> {
|
||||
const res = await request('PUT', '', { interactionEvent: 'SignIn' });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.message || `Failed to initialize sign-in (${res.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyPassword(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
const res = await request('POST', '/verification/password', {
|
||||
identifier: { type: 'username', value: username },
|
||||
password,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
if (res.status === 422) {
|
||||
throw new Error('Invalid username or password');
|
||||
}
|
||||
throw new Error(err.message || `Authentication failed (${res.status})`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data.verificationId;
|
||||
}
|
||||
|
||||
export async function identifyUser(verificationId: string): Promise<void> {
|
||||
const res = await request('POST', '/identification', { verificationId });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.message || `Identification failed (${res.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitInteraction(): Promise<string> {
|
||||
const res = await request('POST', '/submit');
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.message || `Submit failed (${res.status})`);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data.redirectTo;
|
||||
}
|
||||
|
||||
export async function signIn(username: string, password: string): Promise<string> {
|
||||
await initInteraction();
|
||||
const verificationId = await verifyPassword(username, password);
|
||||
await identifyUser(verificationId);
|
||||
return submitInteraction();
|
||||
}
|
||||
Reference in New Issue
Block a user