Team table showed dashes for email and role because the raw Logto
response uses primaryEmail (not email) and excludes org roles.
Enrich each member with normalized email and fetched role name.
Invited users couldn't sign in after password reset because
createAndInviteUser omitted the username field — the sign-in page
sent type:username for non-email input but Logto had no username
to match. Now sets username to the email local part, matching how
createUserWithPassword works for bootstrap admins.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create: if admin email matches an existing Logto user, add them to the
tenant org instead of creating a duplicate account. Only creates a new
user when no match is found and a password is provided.
Delete: before deleting the Logto org, list its members. After org
deletion, delete tenant-only users (those with no remaining org
memberships). Users who belong to other orgs are preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vendor-created tenant admins already have an org membership. When they
land on /onboarding (first login, token lacks org claims), detect the
existing tenant via /api/me and trigger a re-auth to pick up org
membership instead of showing the org name form.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Send branded welcome email to tenant admin after provisioning completes
(includes username and dashboard URL)
- Store admin_email on tenant entity (V004 migration)
- Show admin email in vendor tenant list table and detail page
- Fix ClickHouse cleanup: skip materialized views (can't ALTER DELETE on MVs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove tier from create tenant form (always defaults to STARTER,
controlled via license minting)
- Admin email is now the primary identity field
- Username auto-derived from email local part, optionally overridable
- Set primaryEmail on Logto user at creation (prevents invalid accounts)
- Async tenant delete: PG/ClickHouse cleanup runs after commit instead
of blocking the HTTP response
- Remove legacy /server/* OIDC redirect URIs from bootstrap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server sends /t/{slug}/login as post_logout_redirect_uri on logout but
only /t/{slug} and /t/{slug}/login?local were registered, causing
"post_logout_redirect_uri not registered" error from Logto.
Also removes legacy /server/* redirect URIs from bootstrap (greenfield).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The log appender is now embedded in cameleer-core, so it no longer
needs to be downloaded separately and baked into the runtime base
image. Removes the Maven download step and the Dockerfile COPY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The agent and log-appender SNAPSHOTs were republished under the
io.cameleer groupId after the rebrand. The runtime base image build
was failing because the old com/cameleer paths no longer resolve.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs in the sign-in UI's TOTP MFA enrollment flow:
1. Auth app displayed the PC hostname and "Platform Owner" instead of
"Cameleer" and the user's email. The sign-in UI was rendering Logto's
pre-generated QR code which uses the ENDPOINT hostname as issuer.
Now generates our own otpauth:// URI with proper branding, rendered
client-side via qrcode.react.
2. TOTP code verification returned 400 "Invalid TOTP code". The
verifyTotpSetup() call was missing the required verificationId
parameter — Logto's Experience API needs it to locate the pending
secret during enrollment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Other teams completed their com.cameleer → io.cameleer migration.
Update Maven groupId and Java imports to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chainguard free tier only offers :latest (currently JDK 26, unpinned);
the :openjdk-21 tag requires a paid subscription, breaking CI.
Switch both Dockerfiles to bellsoft/liberica-runtime-container:jre-21-slim-glibc:
- Pinned to JDK 21 LTS
- Smallest image (199 MB vs 441/491 MB)
- glibc-based Alpaquita Linux, sh-only (no bash, no pkg manager)
- Free, multi-arch (amd64 + arm64)
- Has sh — required by cameleer-server's DeploymentExecutor (withCmd "sh -c")
Use nobody:nobody (65534) instead of Chainguard's nonroot (65532).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Note that the loader source now lives at docker/runtime-loader/, that
the contract is owned by cameleer-server's DockerRuntimeOrchestrator
(don't change env vars / mount path / exit codes without a coordinated
commit there), and that cameleer-server's LoaderHardeningIT is the
cross-repo regression guard. Also document the chown-/app/jars line
(strip it and tenant deploys break with "wget: Permission denied").
Move the init-container loader image build from cameleer-server CI into
this repo so all sidecar/infra image builds (runtime-base, postgres,
clickhouse, traefik, logto, and now runtime-loader) live in one place.
The loader is consumed by cameleer-server's DockerRuntimeOrchestrator as
a per-replica init container that fetches the tenant JAR from a signed
URL into a named volume before the main container starts. Source +
Dockerfile copied verbatim from cameleer-server@c2efb7fb (the image with
the volume-permission fix). The published tag path is unchanged
(gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest), so running
tenant servers continue pulling the same image.
Build step matches the runtime-base/postgres/clickhouse/traefik pattern
(unconditional rebuild on every push, sha + branch tags, --provenance=false
for Gitea). cameleer-server will follow up with a commit removing its
loader-build step and switching its LoaderHardeningIT to pull the
published image instead of building from a local Dockerfile.
Server moved GET /agents to /environments/{envSlug}/agents and removed
GET /admin/apps. Replace three broken individual calls with a single
GET /admin/license/usage call that returns all counts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace eclipse-temurin:21-jre-alpine with cgr.dev/chainguard/jre:openjdk-21
for the SaaS management plane image. Use Chainguard's built-in nonroot user
instead of custom adduser. Build stages unchanged (ephemeral).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace eclipse-temurin:21-jre-alpine (musl) with cgr.dev/chainguard/jre:openjdk-21
(Wolfi/glibc, daily CVE refresh, signed images + SBOM). Remove the dead ENTRYPOINT
block — DeploymentExecutor overrides it at container creation anyway.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TOTP otpauth URI issuer changed from "Cameleer" to "Cameleer - <org>"
so authenticator apps display the organization name
- Passkeys without a custom name now show parsed device info (e.g.
"Chrome on Windows") instead of "Unnamed passkey"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Resolve org role names to Logto role IDs in invite and role change flows
(fixes entity.relation_foreign_key_not_found on invite)
- Handle existing Logto users on re-invite instead of failing with
email_already_in_use
- Delete users from Logto when removed from last org membership
- Consolidate tenant settings page into 3 cards: Tenant Details, MFA,
Authentication Policy — remove duplicate MFA Enforcement and Change
Password (now in Account Settings)
- Make passkey list scrollable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same fix as registration verify — @simplewebauthn/browser returns
type: "public-key" but Logto expects type: "WebAuthn".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four fixes for the MFA sign-in flow:
1. Fix passkey verify crash: extract authenticationOptions from Logto
response (was passing full response as optionsJSON). Pass
verificationId to the verify endpoint.
2. Default to passkey verification when no MFA method preference is
stored (was showing method picker which offered TOTP to passkey-only
users).
3. Show backup codes after MFA enrollment: new mfaEnrollBackupCodes
mode with copy/download buttons and confirmation checkbox. Users
must save codes before completing sign-in.
4. Remove duplicate error alerts in enrollment screens (top-level
alert handles all modes).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TOTP secret: /verification/totp/secret (not /verification/totp)
- Backup codes: generate via /verification/backup-code/generate first,
then bind with the returned verificationId. Cannot bind BackupCode
without generating codes first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bind BackupCode after primary MFA factor (WebAuthn or TOTP) to satisfy
Logto's requirement that backup codes accompany any MFA method.
- Add TOTP enrollment option alongside passkey on the enrollment screen:
"Use passkey" / "Use authenticator app" / "Set up later".
- TOTP enrollment shows QR code + secret + 6-digit verification inline
in the sign-in UI, using Experience API endpoints.
- Added createTotpSecret() and verifyTotpSetup() to experience-api.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bootstrap only set [Totp, BackupCode] — WebAuthn was missing. Now
matches LogtoStartupConfig: all three factors available from first boot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fundamental fixes:
- user.missing_mfa now triggers MfaEnrollmentError (enroll UI) instead
of MfaRequiredError (verify UI). Users without MFA were shown a TOTP
code prompt they couldn't fill.
- Logto MFA factors always set to [Totp, WebAuthn, BackupCode] with
UserControlled policy on startup. Availability is always-on for all
users. The vendor auth policy controls enforcement (via
MfaEnforcementFilter), not what Logto offers during sign-in.
- Removed syncMfaConfigToLogto from VendorAuthPolicyController — vendor
policy changes no longer modify Logto's sign-in experience.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Profile API returns empty string instead of "null" when Logto user
has no display name set (String.valueOf(null) → "null" bug).
- SettingsPage: add overflowY auto + flex 1 so content scrolls within
the AppShell layout (which uses overflow: hidden).
- Remove redundant passkey offer from onboarding page — passkey
enrollment now happens during sign-in via the Experience API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When MFA mode is off but passkeys are enabled, WebAuthn + BackupCode
factors are still synced to Logto. Previously, MFA off cleared all
factors including WebAuthn, so passkey enrollment was never offered.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the SaaS backend proxy approach for passkey registration (Account
API binding, Management API proxy, password modal in PasskeySection).
Instead, offer passkey enrollment natively during sign-in via Logto's
Experience API — the correct architectural layer.
Sign-in flow: when Logto returns MFA enrollment available (422), show a
"Secure your account" screen with Register passkey / Set up later. Uses
Experience API WebAuthn registration endpoints. Works for all users
(SaaS and future server users) since the sign-in UI is shared.
PasskeySection in account settings now only manages existing passkeys
(list/rename/delete) and directs users to register during sign-in.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto's /api/my-account/ endpoints reject the opaque access token with
401 even though /api/verifications/ accepts it. The bind step now goes
through the SaaS backend which calls the Management API instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto Account API requires identity verification (logto-verification-id
header) for sensitive MFA operations. Adds a password prompt modal before
the WebAuthn ceremony — verifies password first, then proceeds with
passkey registration using the verification record ID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vendor sidebar collapsed and tenant sidebar appeared when navigating to
/settings/account because onVendorRoute was false for non-/vendor paths.
Now vendor users stay on vendor sidebar for all routes except /tenant/*.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto's PATCH /api/account-center expects mfa as 'Off'|'ReadOnly'|'Edit',
not a nested object. Fixes 400 Bad Request on startup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sync vendor auth policy to Logto sign-in experience on save and on
startup. Always include WebAuthn + TOTP + BackupCode in MFA factors
when MFA is enabled — no reason to gate passkeys behind a toggle.
- Enable Logto Account Center on startup for user-facing MFA management.
- Add passkey registration to account settings via Logto Account API.
Frontend calls Logto directly (same domain) for the WebAuthn ceremony:
generate options, browser credential creation, verify, and bind.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for MFA enrollment and sign-in:
- Defer TOTP registration with Logto until after 6-digit code verification.
Previously setupTotp() immediately registered the secret, so abandoning
enrollment mid-way left MFA active without a working authenticator.
- Move entire MFA enrollment flow (QR code, verify, backup codes) into a
Modal dialog instead of replacing the Card content inline.
- Fix sign-in MFA flow: submitMfa() no longer calls identifyUser() after
TOTP verify — user is already identified, and passing the MFA
verificationId to identification returned 422 ("method not activated").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Repositions the "Forgot password?" link from above the sign-in button
to below it, matching the desired layout. Updates link style to be
centered with link color instead of right-aligned muted text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto's secretQrCode is a data:image/png;base64 URI, not an otpauth://
string. QRCodeSVG crashes trying to encode it ("Data too long"). Now
renders data URIs as <img> and only uses QRCodeSVG for otpauth:// URIs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto's /api/roles/{id}/users endpoint rejects page=1 with
guard.invalid_pagination. Remove explicit pagination params and
let Logto use its defaults.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The TenantPortalService constructor gained an AccountService parameter
in the consolidation refactor — the test was missing it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove dead IllegalArgumentException catch blocks in TenantPortalController
(delegated methods now throw ResponseStatusException, handled by Spring)
- Add password reset notification email in VendorAdminService.resetAdminPassword
- Add verifyIsVendorAdmin guard to resetAdminPassword and resetAdminMfa
to prevent platform admins from resetting arbitrary non-admin users
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>