Generate a unique JWT secret per tenant at provision time, stored on
TenantEntity (same pattern as dbPassword). On upgrade, the existing
secret is reused so agent tokens survive container recreation.
- V005 migration: add jwt_secret column to tenants table
- TenantEntity: add jwtSecret field
- TenantProvisionRequest: add jwtSecret field
- VendorTenantService: generate secret in provisionAsync(), reuse on upgrade
- DockerTenantProvisioner: read from req.jwtSecret() not props
- ProvisioningProperties: remove jwtSecret (no longer global config)
Installer team: CAMELEER_SERVER_SECURITY_JWTSECRET and
CAMELEER_SAAS_PROVISIONING_JWTSECRET can be removed from compose
templates and .env — no longer consumed by the SaaS app.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace hardcoded JWT secret in DockerTenantProvisioner with config
property (CAMELEER_SAAS_PROVISIONING_JWTSECRET) — every provisioned
tenant server was sharing the same publicly-visible dev secret
- Add @PreAuthorize("SCOPE_tenant:manage") to 11 admin endpoints in
TenantPortalController (team invite/remove/role, password resets,
server restart/upgrade, CA cert management, MFA reset) — previously
any org member including viewers could perform admin operations
- Remove dead PATCH /api/tenant/settings endpoint (duplicate of
/auth-settings without authorization) and POST /api/tenant/password
(allowed password change without current password verification) —
frontend uses the secure alternatives
- Add @PreAuthorize("SCOPE_platform:admin") to TenantController
getById and getBySlug — were exposing serverEndpoint, adminEmail,
and provisionError to any authenticated user
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
- 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>
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>
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>
- 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>
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'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>
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>
Vendor admins use global roles, not org roles — passing null orgId
would previously cause addUserToOrganization to call
/api/organizations/null/users and fail.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds verifyUserPassword (for current-password check before password change) and
four global role methods (listRoleUsers, getRoleByName, assignGlobalRole,
revokeGlobalRole) needed by the upcoming AccountService and VendorAdminService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add V002/V003 migrations and VendorAuthPolicy classes to CLAUDE.md
- Document MFA & passkey enforcement model in config CLAUDE.md
- Mark passkey MFA design spec as Implemented
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change auth-settings endpoint from PUT to PATCH (matches partial update semantics and frontend hook)
- Add @PreAuthorize("SCOPE_tenant:manage") to updateAuthSettings endpoint
- Consolidate MFA/passkey 403 redirect handling in API client
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add getAppCount() to ServerApiClient, include usage counts (agents,
environments, apps, users) in tenant license and vendor detail responses
so the frontend can render progress bars against license limits.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OnboardingService passed "LOW" as the tier, but the Tier enum only has
STARTER/TEAM/BUSINESS/ENTERPRISE. Tier.valueOf("LOW") threw
IllegalArgumentException, which the controller caught as a blanket 409
Conflict — masking the real cause. Also catch IllegalStateException
(user already has a tenant) to return 409 instead of 500.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server team extracted license types into cameleer-license-api (#156).
Package moved from com.cameleer.server.core.license to com.cameleer.license.
Dependency tree is now: cameleer-saas → minter → license-api (no server-core).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>