Files
cameleer-saas/ui/sign-in/src/SignInPage.tsx

638 lines
22 KiB
TypeScript
Raw Normal View History

feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
import { type FormEvent, useEffect, useMemo, useState } from 'react';
import { Eye, EyeOff } from 'lucide-react';
import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system';
import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg';
import {
signIn, startRegistration, completeRegistration,
startForgotPassword, forgotPasswordVerifyAndReset,
verifyTotp, verifyBackupCode, submitMfa,
MfaRequiredError,
} from './experience-api';
import styles from './SignInPage.module.css';
type Mode = 'signIn' | 'register' | 'verifyCode' | 'forgotPassword' | 'forgotPasswordVerify' | 'mfaVerify' | 'mfaBackupCode';
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const SIGN_IN_SUBTITLES = [
"Prove you're not a mirage",
"Only authorized cameleers beyond this dune",
"Halt, traveler — state your business",
"The caravan doesn't move without credentials",
"No hitchhikers on this caravan",
"This oasis requires a password",
"Camels remember faces. We use passwords.",
"You shall not pass... without logging in",
"The desert is vast. Your session has expired.",
"Another day, another dune to authenticate",
"Papers, please. The caravan master is watching.",
"Trust, but verify — ancient cameleer proverb",
"Even the Silk Road had checkpoints",
"Your camel is parked outside. Now identify yourself.",
"One does not simply walk into the dashboard",
"The sands shift, but your password shouldn't",
"Unauthorized access? In this economy?",
"Welcome back, weary traveler",
"The dashboard awaits on the other side of this dune",
"Keep calm and authenticate",
"Who goes there? Friend or rogue exchange?",
"Access denied looks the same in every desert",
"May your routes be green and your tokens valid",
"Forgot your password? That's between you and the dunes.",
"No ticket, no caravan",
];
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const REGISTER_SUBTITLES = [
"Every great journey starts with a single sign-up",
"Welcome to the caravan — let's get you registered",
"A new cameleer approaches the oasis",
"Join the caravan. We have dashboards.",
"The desert is better with company",
"First time here? The camels don't bite.",
"Pack your bags, you're joining the caravan",
"Room for one more on this caravan",
"New rider? Excellent. Credentials, please.",
"The Silk Road awaits — just need your email first",
];
function pickRandom(arr: string[]) {
return arr[Math.floor(Math.random() * arr.length)];
}
function getInitialMode(): Mode {
const params = new URLSearchParams(window.location.search);
if (params.get('first_screen') === 'register') return 'register';
if (window.location.pathname.endsWith('/register')) return 'register';
return 'signIn';
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
}
export function SignInPage() {
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const [mode, setMode] = useState<Mode>(getInitialMode);
const [registrationEnabled, setRegistrationEnabled] = useState(true);
const [emailConnectorConfigured, setEmailConnectorConfigured] = useState(false);
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const subtitle = useMemo(
() => pickRandom(mode === 'signIn' ? SIGN_IN_SUBTITLES : REGISTER_SUBTITLES),
[mode === 'signIn' ? 'signIn' : 'register'],
);
const [identifier, setIdentifier] = useState('');
const [password, setPassword] = useState('');
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const [confirmPassword, setConfirmPassword] = useState('');
const [code, setCode] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const [verificationId, setVerificationId] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmNewPassword, setConfirmNewPassword] = useState('');
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
// Fetch sign-in experience to check if registration is enabled
useEffect(() => {
fetch('/api/.well-known/sign-in-exp')
.then((r) => r.json())
.then((data) => {
const enabled = data.signInMode === 'SignInAndRegister';
setRegistrationEnabled(enabled);
if (!enabled && mode !== 'signIn') setMode('signIn');
const hasEmailConnector = Array.isArray(data.signUp?.identifiers) && data.signUp.identifiers.includes('email');
setEmailConnectorConfigured(hasEmailConnector);
})
.catch(() => {});
}, []);
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
// Reset error when switching modes
useEffect(() => { setError(null); }, [mode]);
const switchMode = (next: Mode) => {
setMode(next);
setPassword('');
setConfirmPassword('');
setNewPassword('');
setConfirmNewPassword('');
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
setCode('');
setShowPassword(false);
setVerificationId('');
};
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
// --- Sign-in ---
const handleSignIn = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const redirectTo = await signIn(identifier, password);
window.location.replace(redirectTo);
} catch (err) {
if (err instanceof MfaRequiredError) {
setMode('mfaVerify');
setLoading(false);
return;
}
setError(err instanceof Error ? err.message : 'Sign-in failed');
setLoading(false);
}
};
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
// --- Register step 1: send verification code ---
const handleRegister = async (e: FormEvent) => {
e.preventDefault();
setError(null);
if (!identifier.includes('@')) {
setError('Please enter a valid email address');
return;
}
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setLoading(true);
try {
const vId = await startRegistration(identifier);
setVerificationId(vId);
setMode('verifyCode');
} catch (err) {
setError(err instanceof Error ? err.message : 'Registration failed');
} finally {
setLoading(false);
}
};
// --- Register step 2: verify code + complete ---
const handleVerifyCode = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const redirectTo = await completeRegistration(identifier, password, verificationId, code);
window.location.replace(redirectTo);
} catch (err) {
setError(err instanceof Error ? err.message : 'Verification failed');
setLoading(false);
}
};
// --- Forgot password step 1: send reset code ---
const handleForgotPassword = async (e: FormEvent) => {
e.preventDefault();
setError(null);
if (!identifier.includes('@')) {
setError('Please enter your email address');
return;
}
setLoading(true);
try {
const vId = await startForgotPassword(identifier);
setVerificationId(vId);
setMode('forgotPasswordVerify');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to send reset code');
} finally {
setLoading(false);
}
};
// --- Forgot password step 2: verify code + set new password ---
const handleForgotPasswordVerify = async (e: FormEvent) => {
e.preventDefault();
setError(null);
if (newPassword !== confirmNewPassword) {
setError('Passwords do not match');
return;
}
if (newPassword.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setLoading(true);
try {
await forgotPasswordVerifyAndReset(identifier, verificationId, code, newPassword);
// Send security notification email (fire-and-forget)
fetch('/platform/api/password-reset-notification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: identifier }),
}).catch(() => {});
// Reset to sign-in with success message
switchMode('signIn');
setError(null);
setIdentifier(identifier); // preserve email for convenience
alert('Password reset successful. Please sign in with your new password.');
} catch (err) {
setError(err instanceof Error ? err.message : 'Password reset failed');
} finally {
setLoading(false);
}
};
// --- MFA: TOTP verification ---
const handleMfaVerify = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const verificationId = await verifyTotp(code);
const redirectTo = await submitMfa(verificationId);
window.location.replace(redirectTo);
} catch (err) {
setError(err instanceof Error ? err.message : 'Verification failed');
setLoading(false);
}
};
// --- MFA: backup code verification ---
const handleBackupCodeVerify = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const verificationId = await verifyBackupCode(code);
const redirectTo = await submitMfa(verificationId);
window.location.replace(redirectTo);
} catch (err) {
setError(err instanceof Error ? err.message : 'Verification failed');
setLoading(false);
}
};
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
const passwordToggle = (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className={styles.passwordToggle}
tabIndex={-1}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
);
return (
<div className={styles.page}>
<Card className={styles.card}>
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
<div className={styles.formContainer}>
<div className={styles.logo}>
<img src={cameleerLogo} alt="" className={styles.logoImg} />
Cameleer
</div>
<p className={styles.subtitle}>{subtitle}</p>
{error && (
<div className={styles.error}>
<Alert variant="error">{error}</Alert>
</div>
)}
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
{/* --- Sign-in form --- */}
{mode === 'signIn' && (
<form className={styles.fields} onSubmit={handleSignIn} aria-label="Sign in" noValidate>
<FormField label="Login" htmlFor="login-identifier">
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
<Input
id="login-identifier"
type="email"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
placeholder="you@company.com"
autoFocus
autoComplete="username"
disabled={loading}
/>
</FormField>
<FormField label="Password" htmlFor="login-password">
<div className={styles.passwordWrapper}>
<Input
id="login-password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
autoComplete="current-password"
disabled={loading}
/>
{passwordToggle}
</div>
</FormField>
{emailConnectorConfigured && (
<button
type="button"
className={styles.forgotLink}
onClick={() => { setError(null); setMode('forgotPassword'); }}
>
Forgot password?
</button>
)}
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || !identifier || !password}
className={styles.submitButton}
>
Sign in
</Button>
{registrationEnabled && (
<p className={styles.switchText}>
Don't have an account?{' '}
<button type="button" className={styles.switchLink} onClick={() => switchMode('register')}>
Sign up
</button>
</p>
)}
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
</form>
)}
{/* --- Register form --- */}
{mode === 'register' && (
<form className={styles.fields} onSubmit={handleRegister} aria-label="Create account" noValidate>
<FormField label="Email" htmlFor="register-email">
<Input
id="register-email"
type="email"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
placeholder="you@company.com"
autoFocus
autoComplete="email"
disabled={loading}
/>
</FormField>
<FormField label="Password" htmlFor="register-password">
<div className={styles.passwordWrapper}>
<Input
id="register-password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="At least 8 characters"
autoComplete="new-password"
disabled={loading}
/>
{passwordToggle}
</div>
</FormField>
<FormField label="Confirm password" htmlFor="register-confirm">
<Input
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
id="register-confirm"
type={showPassword ? 'text' : 'password'}
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
autoComplete="new-password"
disabled={loading}
/>
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || !identifier || !password || !confirmPassword}
className={styles.submitButton}
>
Create account
</Button>
<p className={styles.switchText}>
Already have an account?{' '}
<button type="button" className={styles.switchLink} onClick={() => switchMode('signIn')}>
Sign in
</button>
feat: self-service sign-up with email verification and onboarding Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00
</p>
</form>
)}
{/* --- Verification code form --- */}
{mode === 'verifyCode' && (
<form className={styles.fields} onSubmit={handleVerifyCode} aria-label="Verify email" noValidate>
<p className={styles.verifyHint}>
We sent a verification code to <strong>{identifier}</strong>
</p>
<FormField label="Verification code" htmlFor="verify-code">
<Input
id="verify-code"
type="text"
inputMode="numeric"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder="000000"
autoFocus
autoComplete="one-time-code"
disabled={loading}
/>
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || code.length < 6}
className={styles.submitButton}
>
Verify & create account
</Button>
<p className={styles.switchText}>
<button type="button" className={styles.switchLink} onClick={() => switchMode('register')}>
Back
</button>
</p>
</form>
)}
{/* --- Forgot password: email entry --- */}
{mode === 'forgotPassword' && (
<form className={styles.fields} onSubmit={handleForgotPassword} aria-label="Reset password" noValidate>
<p className={styles.verifyHint}>
Enter your email address and we'll send you a code to reset your password.
</p>
<FormField label="Email" htmlFor="forgot-email">
<Input
id="forgot-email"
type="email"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
placeholder="you@company.com"
autoFocus
autoComplete="email"
disabled={loading}
/>
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || !identifier}
className={styles.submitButton}
>
Send reset code
</Button>
<p className={styles.switchText}>
<button type="button" className={styles.switchLink} onClick={() => switchMode('signIn')}>
Back to sign in
</button>
</p>
</form>
)}
{/* --- Forgot password: verify code + new password --- */}
{mode === 'forgotPasswordVerify' && (
<form className={styles.fields} onSubmit={handleForgotPasswordVerify} aria-label="Set new password" noValidate>
<p className={styles.verifyHint}>
We sent a verification code to <strong>{identifier}</strong>
</p>
<FormField label="Verification code" htmlFor="forgot-code">
<Input
id="forgot-code"
type="text"
inputMode="numeric"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder="000000"
autoFocus
autoComplete="one-time-code"
disabled={loading}
/>
</FormField>
<FormField label="New password" htmlFor="forgot-new-password">
<div className={styles.passwordWrapper}>
<Input
id="forgot-new-password"
type={showPassword ? 'text' : 'password'}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="At least 8 characters"
autoComplete="new-password"
disabled={loading}
/>
{passwordToggle}
</div>
</FormField>
<FormField label="Confirm new password" htmlFor="forgot-confirm-password">
<Input
id="forgot-confirm-password"
type={showPassword ? 'text' : 'password'}
value={confirmNewPassword}
onChange={(e) => setConfirmNewPassword(e.target.value)}
placeholder="••••••••"
autoComplete="new-password"
disabled={loading}
/>
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || code.length < 6 || !newPassword || !confirmNewPassword}
className={styles.submitButton}
>
Reset password
</Button>
<p className={styles.switchText}>
<button type="button" className={styles.switchLink} onClick={() => switchMode('forgotPassword')}>
Back
</button>
</p>
</form>
)}
{/* --- MFA: TOTP verification --- */}
{mode === 'mfaVerify' && (
<form className={styles.fields} onSubmit={handleMfaVerify} aria-label="Two-factor authentication" noValidate>
<p className={styles.verifyHint}>
Enter the 6-digit code from your authenticator app.
</p>
<FormField label="Authentication code" htmlFor="mfa-totp-code">
<Input
id="mfa-totp-code"
type="text"
inputMode="numeric"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder="000000"
autoFocus
autoComplete="one-time-code"
disabled={loading}
/>
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || code.length < 6}
className={styles.submitButton}
>
Verify
</Button>
<div className={styles.backupCodeCard}>
<p className={styles.backupCodeText}>Lost your device?</p>
<button
type="button"
className={styles.backupCodeAction}
onClick={() => { setCode(''); setError(null); setMode('mfaBackupCode'); }}
>
Use a backup code
</button>
</div>
</form>
)}
{/* --- MFA: backup code verification --- */}
{mode === 'mfaBackupCode' && (
<form className={styles.fields} onSubmit={handleBackupCodeVerify} aria-label="Backup code verification" noValidate>
<p className={styles.verifyHint}>
Enter one of your 10 backup codes.
</p>
<FormField label="Backup code" htmlFor="mfa-backup-code">
<Input
id="mfa-backup-code"
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter backup code"
autoFocus
autoComplete="off"
disabled={loading}
/>
</FormField>
<Button
variant="primary"
type="submit"
loading={loading}
disabled={loading || !code}
className={styles.submitButton}
>
Verify backup code
</Button>
<p className={styles.switchText}>
<button type="button" className={styles.switchLink} onClick={() => { setCode(''); setError(null); setMode('mfaVerify'); }}>
Use authenticator app instead
</button>
</p>
</form>
)}
</div>
</Card>
</div>
);
}