- Add server:admin/operator/viewer scopes to bootstrap and org roles
- Grant SaaS admin Logto console access via admin:admin role
- Configure sign-in experience with Cameleer branding (colors + logos)
- Add rolesClaim and audience to server OIDC config
- Add server scopes to PublicConfigController for token inclusion
- Permit logo SVGs in SecurityConfig (fix 401 on /platform/logo.svg)
- Add cameleer3 logo SVGs (light + dark) to ui/public/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reflects current state: path-based routing, SaaS at /platform,
Logto catch-all, TLS init container, server integration env vars,
custom JwtDecoder for ES384, skip consent for SSO.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without this, Logto returns consent_required when the server tries
SSO because the scopes were never explicitly granted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server OIDC callback is at /oidc/callback (without /server/ prefix due
to strip-prefix). Register both variants until server reads
X-Forwarded-Prefix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser sends Origin header on fetch calls even same-origin. Server
needs the public host in its CORS allowlist. Derived from PUBLIC_HOST.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Self-signed certs cause PKIX errors when the server fetches OIDC
discovery. CAMELEER_OIDC_TLS_SKIP_VERIFY=true disables cert
verification for OIDC calls only (server-team feature, pending build).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nginx needs to see /assets/... not /server/assets/... to find the files.
Strip-prefix + BASE_PATH=/server now works correctly with the fixed image.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server team fixed the BASE_PATH sed ordering bug. Remove our entrypoint
override and let the image's own entrypoint handle it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The server-ui's entrypoint inserts <base href> THEN rewrites all
href="/" — including the just-inserted base tag, causing doubling.
Patched entrypoint rewrites asset paths first, then inserts <base>.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-ui needs BASE_PATH for React Router basename. Without strip-prefix,
no X-Forwarded-Prefix doubling. Server-ui handles full /server/ path itself.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-ui injects BASE_PATH=/server/ into <base href>. Without strip-prefix,
Traefik forwards /server/ path AND server-ui adds /server/ again = double prefix.
Strip /server before forwarding so server-ui sees / and produces correct <base href="/server/">.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LoginPage and useAuth used window.location.origin without the /platform
base path, causing redirect_uri mismatch with Logto's registered URIs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-side path rewrite breaks React Router (browser URL stays at /
but basename is /platform). The SPA entry point is /platform/ — users
bookmark that. Root / goes to Logto catch-all which is correct for
direct OIDC flows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users hitting the root URL now get redirected to the SaaS app instead
of seeing Logto's unknown-session page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto's sign-in form calls /api/interaction/* and /api/experience/* to
submit credentials, but these were routed to cameleer-saas by the /api
catch-all. Added explicit Logto API paths with higher Traefik priority.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto uses root-level paths for its sign-in UI (/sign-in, /register,
/consent, /single-sign-on, /social, /unknown-session) that were falling
through to the SPA catch-all and getting 401.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SPA assets moved from /assets/ to /_app/ for single-domain routing,
but SecurityConfig still permitted the old path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server-ui now handles base path natively via BASE_PATH env var.
Traefik forwards full path without stripping.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move SPA assets from /assets/ to /_app/ (Vite assetsDir config) so
Traefik can route /assets/* to Logto without conflict. All services
on one hostname with path-based routing:
- /oidc/*, /interaction/*, /assets/* → Logto
- /server/* → server-ui (prefix stripped)
- /api/* → cameleer-saas
- /* (catch-all) → cameleer-saas SPA
Customer needs only 1 DNS record. Server gets OIDC_JWK_SET_URI for
Docker-internal JWK fetch (standard Spring split config).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Path-based routing on one hostname. SPA assets move to /_app/,
Logto gets /assets/ + /oidc/ + /interaction/. Server-ui at /server/.
Includes requirements for server team (split JWK/issuer, BASE_PATH).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standard OIDC architecture: subdomain routing (auth.HOST, server.HOST),
TLS via Traefik, self-signed cert auto-generated on first boot.
- Add traefik-certs init container (generates wildcard self-signed cert)
- Enable TLS on all Traefik routers (websecure entrypoint)
- HTTP→HTTPS redirect in traefik.yml
- Host-based routing for all services (no more path conflicts)
- PUBLIC_PROTOCOL env var (https default, configurable)
- Protocol-aware redirect URIs in bootstrap
- Protocol-aware UI fallbacks
Customer bootstrap: set PUBLIC_HOST + DNS records + docker compose up.
For production TLS, configure Traefik ACME (Let's Encrypt).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server UI assets also use absolute /assets/* paths that conflict with
the SPA catch-all. Same fix as Logto: Host-based routing at
server.localhost gives it its own namespace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto's login page references /assets/* which conflicts with the SPA's
assets at the same path. Using Host-based routing (auth.localhost) gives
Logto its own namespace - all paths on that subdomain go to Logto,
eliminating the conflict. *.localhost resolves to 127.0.0.1 and is a
secure context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto ENDPOINT now points at Traefik (http://localhost) instead of
directly at port 3001. All services share the same base URL, eliminating
OIDC issuer mismatches and crypto.subtle secure context issues.
- Remove :3001 from all public-facing Logto URLs
- Add cameleer3-server-ui to Traefik at /server/ with prefix strip
- Dashboard link uses /server/ path instead of port 8082
- Bootstrap Host headers match Logto ENDPOINT (no port)
- Redirect URIs simplified (Traefik handles port 80)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crypto.subtle requires a secure context, so the browser must access
everything via localhost. The custom JwtDecoder already supports this
split: jwk-set-uri uses Docker-internal logto:3001 for network fetch,
while issuer-uri uses localhost:3001 for string-only claim validation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All public-facing URLs (Logto OIDC, redirect URIs, dashboard links) now
derive from PUBLIC_HOST in .env instead of scattered localhost references.
Resolves Docker networking ambiguity where localhost inside containers
doesn't reach the host machine.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bootstrap was sending LOGTO_PUBLIC_ENDPOINT (http://localhost:3001)
as the OIDC issuer URI to the server. Inside Docker, localhost is
unreachable. Changed to LOGTO_ENDPOINT (http://logto:3001).
Also: .env must set LOGTO_ISSUER_URI=http://logto:3001/oidc (not
localhost) since this env var feeds cameleer3-server's OIDC decoder.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 7 server health check failed intermittently due to timing.
Add 3-attempt retry loop with 2s sleep. Guard against jq returning
literal "null" string for TRAD_SECRET. Add debug logging for both
preconditions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cameleer3-server deploys backend and UI as separate containers.
Add the cameleer3-server-ui image (nginx SPA + API reverse proxy)
to the Compose stack, exposed on port 8082 in dev. Update sidebar
"View Dashboard" link to point to the UI container.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConnectivityHealthCheck: /actuator/health → /api/v1/health (actuator
now requires auth on cameleer3-server after OIDC was added).
Bootstrap: POST → PUT for /api/v1/admin/oidc (server expects PUT,
POST returned 405 causing OIDC config to silently fail).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace TenantResolutionFilter + TenantOwnershipValidator (15 manual
calls across 5 controllers) with a single TenantIsolationInterceptor
that uses Spring HandlerMapping path variables for fail-closed tenant
isolation. New endpoints with {tenantId}, {environmentId}, or {appId}
path variables are automatically isolated without manual code.
Simplify OrgResolver from dual-token fetch to single token — Logto
merges all scopes into either token type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add @PreAuthorize annotations to all API controllers (14 endpoints
across 6 controllers) enforcing OAuth2 scopes: apps:manage, apps:deploy,
billing:manage, observe:read, platform:admin.
Enforce tenant isolation: TenantResolutionFilter now rejects cross-tenant
access on /api/tenants/{id}/* paths. New TenantOwnershipValidator checks
environment/app ownership for paths without tenantId. Platform admins
bypass both layers.
Fix frontend: OrgResolver split into two useEffect hooks so scopes
refresh on org switch. Scopes now served from /api/config (single source
of truth). Bootstrap cleaned — standalone org permissions removed.
Update docs/architecture.md, docs/user-manual.md, and CLAUDE.md to
reflect all auth hardening changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive technical reference covering system topology, auth model
(Logto OIDC, scopes, token types, Spring Security pipeline), data model
(7 tables from Flyway migrations), deployment flow, agent-server protocol,
API endpoints, security boundaries, frontend architecture, and full
configuration reference. All class names, paths, and properties verified
against the codebase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove role-to-permission mapping (usePermissions, RequirePermission) and replace
with direct scope reads from the Logto access token JWT. OrgResolver decodes the
scope claim after /api/me resolves and stores scopes in Zustand. RequireScope and
useScopes replace the old hooks/components across all pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates 10 API resource scopes (platform:admin + 9 tenant-level) on the
Cameleer SaaS API resource, assigns all to platform-admin role, creates
matching organization scopes, and wires them declaratively to org roles
(admin gets all, member gets deploy/observe). All operations are idempotent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ProtectedRoute: only gate on initial auth load, not every async op
- TokenSync: OrgResolver is sole source of org data, remove fetchUserInfo
- docker-compose.dev: mount ui/dist for hot-reload
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @Primary + @ConditionalOnMissingBean so TestSecurityConfig.jwtDecoder()
wins over SecurityConfig.jwtDecoder() without needing a real OIDC endpoint
- Add spring.main.allow-bean-definition-overriding=true and
cameleer.clickhouse.enabled=false to src/test/resources/application-test.yml
so Testcontainers @ServiceConnection can supply the datasource
- Disable ClickHouse in test profile (src/main/resources/application-test.yml)
so the explicit ClickHouseConfig DataSource bean is not created, allowing
@ServiceConnection to wire the Testcontainers Postgres datasource
- Fix TenantControllerTest and LicenseControllerTest to explicitly grant
ROLE_platform-admin authority via .authorities() on the test JWT, since
spring-security-test does not run the custom JwtAuthenticationConverter
- Fix EnvironmentService.createDefaultForTenant() to use an internal
bootstrap path that skips license enforcement (chicken-and-egg: no license
exists at tenant creation time yet)
- Remove now-unnecessary license stub from EnvironmentServiceTest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>