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>
This commit is contained in:
@@ -42,11 +42,13 @@ Server containers join three networks: tenant network (primary), shared services
|
||||
|
||||
## Custom sign-in UI (`ui/sign-in/`)
|
||||
|
||||
Separate Vite+React SPA replacing Logto's default sign-in page. Visually matches cameleer-server LoginPage.
|
||||
Separate Vite+React SPA replacing Logto's default sign-in page. Supports both sign-in and self-service registration.
|
||||
|
||||
- Built as custom Logto Docker image (`cameleer-logto`): `ui/sign-in/Dockerfile` = node build stage + `FROM ghcr.io/logto-io/logto:latest` + COPY dist over `/etc/logto/packages/experience/dist/`
|
||||
- Built as custom Logto Docker image (`cameleer-logto`): `ui/sign-in/Dockerfile` = node build stage + `FROM ghcr.io/logto-io/logto:latest` + install official connectors (SMTP) + COPY dist over `/etc/logto/packages/experience/dist/`
|
||||
- Uses `@cameleer/design-system` components (Card, Input, Button, FormField, Alert)
|
||||
- Authenticates via Logto Experience API (4-step: init -> verify password -> identify -> submit -> redirect)
|
||||
- **Sign-in**: Logto Experience API (4-step: init -> verify password -> identify -> submit -> redirect). Auto-detects email vs username identifier.
|
||||
- **Registration**: 2-phase flow. Phase 1: init Register -> send verification code to email. Phase 2: verify code -> set password -> identify (creates user) -> submit -> redirect.
|
||||
- Reads `first_screen=register` from URL query params to show register form initially (set by `@logto/react` SDK's `firstScreen` option)
|
||||
- `CUSTOM_UI_PATH` env var does NOT work for Logto OSS — must volume-mount or replace the experience dist directory
|
||||
- Favicon bundled in `ui/sign-in/public/favicon.svg` (served by Logto, not SaaS)
|
||||
|
||||
@@ -81,8 +83,12 @@ Idempotent script run inside the Logto container entrypoint. **Clean slate** —
|
||||
5. Create admin user (SaaS admin with Logto console access)
|
||||
7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: owner->server:admin, operator->server:operator, viewer->server:viewer; saas-vendor global role -> server:admin)
|
||||
8. Configure Logto sign-in branding (Cameleer colors `#C6820E`/`#D4941E`, logo from `/platform/logo.svg`)
|
||||
8b. Configure SMTP email connector (if `SMTP_HOST`/`SMTP_USER` env vars set) — discovers factory via `/api/connector-factories`, creates connector with Cameleer-branded HTML email templates for Register/SignIn/ForgotPassword/Generic. Skips gracefully if SMTP not configured.
|
||||
8c. Enable self-service registration — sets `signInMode: "SignInAndRegister"`, `signUp: { identifiers: ["email"], password: true, verify: true }`, sign-in methods: email+password and username+password (backwards-compatible with admin user).
|
||||
9. Cleanup seeded Logto apps
|
||||
10. Write bootstrap results to `/data/logto-bootstrap.json`
|
||||
12. Create `saas-vendor` global role with all API scopes and assign to admin user (always runs — admin IS the platform admin).
|
||||
|
||||
SMTP env vars for email verification: `SMTP_HOST`, `SMTP_PORT` (default 587), `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM_EMAIL` (default `noreply@cameleer.io`). Passed to `cameleer-logto` container via docker-compose. Both installers prompt for these in SaaS mode.
|
||||
|
||||
The multi-tenant compose stack is: Traefik + PostgreSQL + ClickHouse + Logto (with bootstrap entrypoint) + cameleer-saas. No `cameleer-server` or `cameleer-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`.
|
||||
|
||||
@@ -564,6 +564,123 @@ api_patch "/api/sign-in-exp" "{
|
||||
}"
|
||||
log "Sign-in branding configured."
|
||||
|
||||
# ============================================================
|
||||
# PHASE 8b: Configure SMTP email connector
|
||||
# ============================================================
|
||||
# Required for email verification during registration and password reset.
|
||||
# Skipped if SMTP_HOST is not set (registration will not work without email delivery).
|
||||
|
||||
if [ -n "${SMTP_HOST:-}" ] && [ -n "${SMTP_USER:-}" ]; then
|
||||
log "Configuring SMTP email connector..."
|
||||
|
||||
# Discover available email connector factories
|
||||
FACTORIES=$(api_get "/api/connector-factories")
|
||||
# Prefer a factory with "smtp" in the ID
|
||||
SMTP_FACTORY_ID=$(echo "$FACTORIES" | jq -r '[.[] | select(.type == "Email" and (.id | test("smtp"; "i")))] | .[0].id // empty')
|
||||
if [ -z "$SMTP_FACTORY_ID" ]; then
|
||||
# Fall back to any non-demo Email factory
|
||||
SMTP_FACTORY_ID=$(echo "$FACTORIES" | jq -r '[.[] | select(.type == "Email" and .isDemo != true)] | .[0].id // empty')
|
||||
fi
|
||||
|
||||
if [ -n "$SMTP_FACTORY_ID" ]; then
|
||||
# Build SMTP config JSON
|
||||
SMTP_CONFIG=$(jq -n \
|
||||
--arg host "$SMTP_HOST" \
|
||||
--arg port "${SMTP_PORT:-587}" \
|
||||
--arg user "$SMTP_USER" \
|
||||
--arg pass "${SMTP_PASS:-}" \
|
||||
--arg from "${SMTP_FROM_EMAIL:-noreply@cameleer.io}" \
|
||||
'{
|
||||
host: $host,
|
||||
port: ($port | tonumber),
|
||||
auth: { user: $user, pass: $pass },
|
||||
fromEmail: $from,
|
||||
templates: [
|
||||
{
|
||||
usageType: "Register",
|
||||
contentType: "text/html",
|
||||
subject: "Verify your email for Cameleer",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to verify your email and create your account:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request this, you can safely ignore this email.</p></div>"
|
||||
},
|
||||
{
|
||||
usageType: "SignIn",
|
||||
contentType: "text/html",
|
||||
subject: "Your Cameleer sign-in code",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your sign-in verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
|
||||
},
|
||||
{
|
||||
usageType: "ForgotPassword",
|
||||
contentType: "text/html",
|
||||
subject: "Reset your Cameleer password",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Enter this code to reset your password:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes. If you did not request a password reset, you can safely ignore this email.</p></div>"
|
||||
},
|
||||
{
|
||||
usageType: "Generic",
|
||||
contentType: "text/html",
|
||||
subject: "Your Cameleer verification code",
|
||||
content: "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:24px\"><span style=\"font-size:24px;font-weight:700;color:#C6820E\">Cameleer</span></div><p style=\"color:#333;font-size:15px;line-height:1.6\">Your verification code:</p><div style=\"text-align:center;margin:24px 0\"><span style=\"font-size:32px;font-weight:700;letter-spacing:6px;color:#C6820E\">{{code}}</span></div><p style=\"color:#666;font-size:13px\">This code expires in 10 minutes.</p></div>"
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
# Check if an email connector already exists
|
||||
EXISTING_CONNECTORS=$(api_get "/api/connectors")
|
||||
EMAIL_CONNECTOR_ID=$(echo "$EXISTING_CONNECTORS" | jq -r '[.[] | select(.type == "Email")] | .[0].id // empty')
|
||||
|
||||
if [ -n "$EMAIL_CONNECTOR_ID" ]; then
|
||||
api_patch "/api/connectors/$EMAIL_CONNECTOR_ID" "{\"config\": $SMTP_CONFIG}" >/dev/null 2>&1
|
||||
log "Updated existing email connector: $EMAIL_CONNECTOR_ID"
|
||||
else
|
||||
CONNECTOR_RESPONSE=$(api_post "/api/connectors" "{\"connectorId\": \"$SMTP_FACTORY_ID\", \"config\": $SMTP_CONFIG}")
|
||||
CREATED_ID=$(echo "$CONNECTOR_RESPONSE" | jq -r '.id // empty')
|
||||
if [ -n "$CREATED_ID" ]; then
|
||||
log "Created SMTP email connector: $CREATED_ID (factory: $SMTP_FACTORY_ID)"
|
||||
else
|
||||
log "WARNING: Failed to create SMTP connector. Response: $(echo "$CONNECTOR_RESPONSE" | head -c 300)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "WARNING: No email connector factory found — email delivery will not work."
|
||||
log "Available factories: $(echo "$FACTORIES" | jq -c '[.[] | select(.type == "Email") | .id]')"
|
||||
fi
|
||||
else
|
||||
log "SMTP not configured (SMTP_HOST/SMTP_USER not set) — email delivery disabled."
|
||||
log "Set SMTP_HOST, SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL env vars to enable."
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# PHASE 8c: Enable registration (email + password)
|
||||
# ============================================================
|
||||
# Configures sign-in experience to allow self-service registration with email verification.
|
||||
# This runs AFTER the SMTP connector so email delivery is ready before registration opens.
|
||||
|
||||
log "Configuring sign-in experience for registration..."
|
||||
api_patch "/api/sign-in-exp" '{
|
||||
"signInMode": "SignInAndRegister",
|
||||
"signUp": {
|
||||
"identifiers": ["email"],
|
||||
"password": true,
|
||||
"verify": true
|
||||
},
|
||||
"signIn": {
|
||||
"methods": [
|
||||
{
|
||||
"identifier": "email",
|
||||
"password": true,
|
||||
"verificationCode": false,
|
||||
"isPasswordPrimary": true
|
||||
},
|
||||
{
|
||||
"identifier": "username",
|
||||
"password": true,
|
||||
"verificationCode": false,
|
||||
"isPasswordPrimary": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}' >/dev/null 2>&1
|
||||
log "Sign-in experience configured: SignInAndRegister (email + password)."
|
||||
|
||||
# ============================================================
|
||||
# PHASE 9: Cleanup seeded apps
|
||||
# ============================================================
|
||||
|
||||
Reference in New Issue
Block a user