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>
The root redirect rule matches Host(`PUBLIC_HOST`), not localhost.
Curl with --resolve (bash) and Host header (PS1) so the health
check sends the right hostname when verifying Traefik routing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both simple and expert modes now ask "Pull images from a private
registry?" with follow-up prompts for URL, username, and token.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both installers (bash + PS1) now support pulling images from a
custom Docker registry. Writes *_IMAGE env vars to .env so compose
templates use the configured registry. Runs docker login before
pull when credentials are provided. Persisted in cameleer.conf
for upgrades/reconfigure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`tr | head -c 32` causes tr to receive SIGPIPE when head exits early.
With `set -eo pipefail`, exit code 141 kills the script right after
"Configuration validated" before any passwords are generated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The docker-builder container runs ash/sh, not bash — arrays with ()
are not supported. Use a simple for-in loop instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>