- Add security notification email after password reset (warns MFA
was not required, recommends enabling it)
- Use distinct APP_MFA_REQUIRED error code + X-Cameleer-Error header
for MFA enforcement 403s to avoid collision with generic access denied
- Make backup code fallback prominent in MFA verification UI (visible
secondary action, not a subtle link)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers self-service password reset via Logto Experience API,
TOTP + backup code MFA with per-tenant enforcement via JWT claims,
and a server handoff document for cameleer-server MFA enrollment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Email-registered users have no name field in Logto, causing empty OIDC
name claims. After adding user to org, derive display name from email
local part (john.doe@acme.com -> john.doe) if name is not already set.
Also adds updateUserProfile() to LogtoManagementClient.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove width/height HTML attributes and add border:0;outline:none to
the watermark img tag so broken-image placeholders collapse gracefully
when email clients block remote images.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After creating a tenant, the existing Logto tokens don't include the new
org membership/scopes. A hard page reload reused stale tokens, causing
the SDK to either lose auth state (redirect loop to login) or fail to
resolve org scopes (falling through to server UI instead of tenant UI).
Replace window.location.href with signIn() to trigger a fresh OIDC flow.
The existing Logto session cookie means auto-approval — no login form.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add GET /api/onboarding/slug-available endpoint to check if a slug is
already taken. Frontend checks availability with 400ms debounce as the
user types and shows inline feedback. Submit button disabled when slug
is taken. POST /api/onboarding/tenant now returns 409 instead of 500
for duplicate slugs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defer provisionAsync() until after the transaction commits using
TransactionSynchronization.afterCommit(). Previously the @Async thread
raced the @Transactional commit — findById returned null because the
tenant INSERT wasn't visible yet.
Downgrade ClickHouse UNKNOWN_TABLE errors to DEBUG level in
InfrastructureService. These are expected on fresh installs before any
cameleer-server has created the tables.
Make the onboarding slug field read-only (derived from org name).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix vertical alignment of Lucide icons inside Button children across
all pages by adding verticalAlign offsets (-3px for 16px icons, -2px
for 14px icons). The design system Button wraps children in an inline
span, so SVG icons defaulted to baseline alignment.
Hide the redundant top-right "Create Tenant" button on VendorTenantsPage
when no tenants exist — the EmptyState already provides that action.
Add icons to all vendor sidebar sub-items for consistency (previously
only Email Connector had one).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sign-in experience must always include both email+password and
username+password methods. The admin user signs in with their email
(admin@company.com) which the sign-in UI detects as email type.
With only username method enabled, Logto rejects it with "this
sign-in method is not activated."
Fixes both bootstrap Phase 8c and EmailConnectorService disable path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto rejects @ in usernames. Extract local part (before @) as the
Logto username, use full email as primaryEmail. Also validates admin
user creation succeeded (logs error instead of silently continuing
with null ID).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sign-in instructions: "Enter your email" (not "email or username")
- Troubleshooting: remove reference to deleted "Sign in with Logto" button
- Sidebar navigation: replace outdated single table with vendor console
and tenant portal sections reflecting current sidebar structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In SaaS mode, SAAS_ADMIN_USER must be an email address. It's used as
both the Logto username and primaryEmail. No separate SAAS_ADMIN_EMAIL.
Installer enforces email format in SaaS mode (moved deployment mode
question before admin credentials), accepts any username in standalone.
Sign-in form label changed to "Login".
Removes SAAS_ADMIN_EMAIL from bootstrap, compose template, installers,
and all documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All users in SaaS mode must have an email address. The bootstrap creates
the admin user with primaryEmail set to SAAS_ADMIN_EMAIL (defaults to
<SAAS_ADMIN_USER>@<PUBLIC_HOST>). This prevents the admin from being
locked out when self-service registration (which requires email) is
enabled via the Email Connector UI.
Documentation updated across all CLAUDE.md files, .env.example,
user-manual.md, and installer submodule (README, .env.example, compose).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When registration is disabled, signUp.identifiers must be reset to
["username"] with verify:false. Otherwise Logto enforces email as a
mandatory profile field on all users, blocking username-only users
(like the admin) from signing in.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fetch /api/.well-known/sign-in-exp on mount and check signInMode.
If not SignInAndRegister, hide the "Sign up" link and force sign-in
mode (even if ?first_screen=register was in the URL).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use same layout as SignInPage: bg-base background, 400px card,
Cameleer logo with text header, matching font sizes and spacing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto does exact-match on post_logout_redirect_uri, so ?signed_out
caused "not registered" error. Use sessionStorage flag instead —
set before signOut, read and cleared on LoginPage mount.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After logout, redirect to /platform/login?signed_out which shows a
"Signed out" card with a "Sign in again" button instead of immediately
redirecting back to Logto OIDC (which would auto-authenticate if the
Logto session cookie persists).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move email connector configuration from installer/bootstrap into the
vendor admin UI for runtime control over SMTP delivery and self-service
registration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Logto entrypoint builds DB_URL from PG_USER/PG_PASSWORD/PG_HOST with
URL-encoding via node's encodeURIComponent, instead of embedding the
raw password in the connection string
- Installer submodule updated: passwords single-quoted in .env/.conf
Fixes SMTP and DB auth failures when passwords contain $, &, ;, [, etc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Customer-facing image defaults now reference the public registry URL.
Updates installer templates and Spring Boot provisioning defaults.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reflect that installer/ is now a git submodule pointing to the public
cameleer-saas-installer repo, and that docker-compose.yml is a thin
dev overlay chained via COMPOSE_FILE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The installer (install.sh, templates/, bootstrap scripts) now lives in
cameleer/cameleer-saas-installer (public repo). Added as a git submodule
at installer/ so compose templates remain the single source of truth.
Dev compose is now a thin overlay (ports + volume mount + dev env vars).
Production templates are chained via COMPOSE_FILE in .env:
installer/templates/docker-compose.yml
installer/templates/docker-compose.saas.yml
docker-compose.yml (dev overrides)
No code duplication — fixes to compose templates go to the installer
repo and propagate to both production deployments and dev via submodule.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 32px card padding and reduce max-width to 420px for consistency
with the sign-in page. Add noValidate to prevent browser-native
required validation outlines on inputs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Newer Logto versions redirect to /register?app_id=... instead of
/sign-in?first_screen=register. Check the pathname in addition to
the query param so the registration form shows correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These routes were missing from SpaController, so requests to
/platform/register and /platform/onboarding had no handler. Spring
forwarded to /error, which isn't in the permitAll() list, resulting
in a 401 instead of serving the SPA.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show "Please enter a valid email address" when the user enters a
username instead of an email in the sign-up form, rather than letting
it hit Logto's API and returning a cryptic 400.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto validates M2M tokens by fetching its own JWKS from the ENDPOINT
URL (e.g. https://app.cameleer.io/oidc/jwks). Behind a Cloudflare
tunnel, that hostname resolves to Cloudflare's IP and the container
can't route back through the tunnel — the fetch times out (ETIMEDOUT),
causing all Management API calls to return 500.
Adding extra_hosts maps AUTH_HOST to host-gateway so the request goes
to the Docker host, which has Traefik on :443, which routes back to
Logto internally. This hairpin works because NODE_TLS_REJECT=0 accepts
the self-signed cert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The upgrade path in handle_rerun called load_config_file a second time
(already called by detect_existing_install). On the second pass, every
variable is already set, so [ -z "$VAR" ] && VAR="$value" returns
exit code 1 (test fails, && short-circuits). With set -e, the non-zero
exit from the case clause kills the script silently after printing
"[INFO] Upgrading installation..." — no error, no further output.
Removed the redundant load_config_file and load_env_overrides calls.
Both were already executed in main() before handle_rerun is reached.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Traefik auto-calculates router priority from rule string length. When
deployed with a domain longer than 23 chars (e.g. app.cameleer.io),
Host(`app.cameleer.io`) (25 chars) outranks PathPrefix(`/platform`)
(23 chars), causing ALL requests — including /platform/* — to route
to Logto instead of the SaaS app. This breaks login because the sign-in
UI loads without an OIDC interaction session.
Setting priority=1 makes Logto a true catch-all, matching the intent
documented in docker/CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>