- Read user profile from Logto ID token in OrgResolver, store in
Zustand org store, display in sidebar footer and TopBar avatar
- Fix license limits showing "—" by aligning frontend LIMIT_LABELS
keys with backend snake_case convention (max_agents, retention_days,
max_environments)
- Bump @cameleer/design-system to v0.1.38 (font-size floor)
- Add dev volume mount for local UI hot-reload without image rebuild
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sidebar, sign-in page, and favicons all use the single SVG
- Postinstall copies SVG for SaaS HTML favicon (gitignored)
- Sign-in favicon committed (baked into Logto Docker image)
- Remove old PNG favicon references
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Dockerfile copies package.json before ui/ contents, so public/
doesn't exist during npm ci. Skip the copy gracefully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sidebar and sign-in logos use Vite import from @cameleer/design-system
- HTML favicons copied by postinstall script (gitignored)
- Remove manually copied PNGs from repo
- Clean up SecurityConfig permitAll (bundled assets under /_app/**)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Maven .m2 and npm caches persist across --no-cache builds, avoiding
full dependency re-downloads on every CI run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change chmod 600 to 644 on bootstrap JSON (cameleer user needs read)
- Use PNG favicon instead of SVG (currentColor invisible in browser tab)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace favicon SVG with official camel-logo.svg from design-system
- Add PNG favicons (32px, 192px) with proper link tags in index.html
- Replace sidebar logo with 48px brand icon (cameleer-logo-48.png)
- Replace sign-in page logo with 48px brand icon
- Permit favicon PNGs in SecurityConfig
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sign-in page is served by Logto, not the SaaS app. Referencing
/platform/favicon.svg required SecurityConfig permitAll and cross-service
routing. Instead, bundle favicon.svg directly in the sign-in UI dist
so Logto serves it at /favicon.svg.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add docker/logto.Dockerfile: builds custom Logto image with sign-in
UI baked into /etc/logto/packages/experience/dist/
- Remove sign-in-ui init container, signinui volume, CUSTOM_UI_PATH
(CUSTOM_UI_PATH is Logto Cloud only, not available in OSS)
- Remove sign-in build stage from SaaS Dockerfile (now in logto.Dockerfile)
- Remove docker/saas-entrypoint.sh (no longer needed)
- LoginPage auto-redirects to Logto OIDC on mount instead of showing
"Sign in with Logto" button — seamless sign-in experience
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Logto's default sign-in page with a custom React SPA that
matches the cameleer3-server login page using @cameleer/design-system.
- New Vite+React app at ui/sign-in/ with Experience API integration
- 4-step auth flow: init → verify password → identify → submit
- Design-system components: Card, Input, Button, FormField, Alert
- Same witty random subtitles as cameleer3-server LoginPage
- Dockerfile: add sign-in-frontend build stage, copy dist to image
- docker-compose: CUSTOM_UI_PATH on Logto, shared signinui volume
- SaaS entrypoint copies sign-in dist to shared volume on startup
- Add .gitattributes for LF line endings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
Replace ID-token claim reads with org store lookups in useAuth and
usePermissions; add currentOrgRoles to useOrgStore; update role names
to Logto org role conventions (admin/member); remove username from
Layout (no longer derived from token claims).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TokenSync sets currentOrgId from fetchUserInfo (no tenantId).
OrgResolver later calls setOrganizations with enriched data including
tenantId. Now setOrganizations auto-resolves currentTenantId when
the org is already selected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /api/me call races with TokenSync — fires before the token
provider is set. Removed the hard window.location redirect on 401
from the API client. React Query retries with backoff instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bootstrap script now creates:
- SaaS Owner (admin/admin) with platform-admin role
- Tenant Admin (camel/camel) in Example Tenant org
- Traditional Web App for cameleer3-server OIDC
- DB records: tenant, default environment, license
- Configures cameleer3-server OIDC via its admin API
All credentials configurable via env vars.
Backend:
- Fix LogtoManagementClient resource URL (https://default.logto.app/api)
- Add getUserRoles/getUserOrganizations to LogtoManagementClient
- Add GET /api/me endpoint (user info, platform admin status, tenants)
- Add GET /api/tenants list-all for platform admins
- Remove insecure X-header forwarding from Traefik
Frontend:
- Org-scoped tokens: getAccessToken(resource, orgId) for tenant context
- OrgResolver component populates org store from /api/me
- useOrganization Zustand store (currentOrgId + currentTenantId)
- Platform admin sidebar section + AdminTenantsPage
- View Dashboard link points to cameleer3-server on port 8081
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The hand-rolled OIDC flow (manual PKCE, token exchange, URL
construction) was fragile and accumulated multiple bugs. Replaced
with the official @logto/react SDK which handles PKCE, token
exchange, storage, and refresh automatically.
- Add @logto/react SDK dependency
- Add LogtoProvider with runtime config in main.tsx
- Add TokenSync component bridging SDK tokens to API client
- Add useAuth hook replacing Zustand auth store
- Simplify LoginPage to signIn(), CallbackPage to useHandleSignInCallback()
- Delete pkce.ts and auth-store.ts (replaced by SDK)
- Fix react-router-dom → react-router imports in page files
- All 17 React Query hooks unchanged (token provider pattern)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto returns opaque access tokens when no resource is specified.
Added API resource creation to bootstrap, included resource indicator
in /api/config, and SPA now passes resource parameter in auth request.
Also fixed issuer-uri to match Logto's public endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logto requires PKCE (Proof Key for Code Exchange) for SPA auth.
Added code_challenge/code_verifier to login and callback flow.
Also fixed Traefik router-service linking — when a container defines
multiple routers, each needs an explicit service binding or Traefik
v3 refuses to auto-link them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- logto-bootstrap.sh: API-driven init script that creates SPA app,
M2M app, and default user (camel/camel) via Logto Management API.
Reads m-default secret from DB, then removes seeded apps with
known secrets (security hardening). Idempotent.
- PublicConfigController: /api/config public endpoint serves Logto
client ID from bootstrap output file (runtime, not build-time)
- Frontend: LoginPage + CallbackPage fetch config from /api/config
instead of import.meta.env (fixes Vite build-time baking issue)
- Docker Compose: logto-bootstrap init service with health-gated
dependency chain, shared volume for bootstrap config
- SecurityConfig: permit /api/config without auth
Flow: docker compose up → bootstrap creates apps/user → SPA fetches
config → login page shows → sign in with Logto → camel/camel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The relative outDir '../src/main/resources/static' resolved
unpredictably in Docker. Use standard 'dist/' output, then:
- Dockerfile: COPY --from=frontend /ui/dist/ to static/
- CI: cp -r dist/ to src/main/resources/static/
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full AppDetailPage with tabbed layout (Overview / Deployments / Logs),
current deployment status with auto-poll, action bar (deploy/stop/restart/re-upload/delete),
agent status card, routing card with edit modal, deployment history table,
and container log viewer with stream filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements EnvironmentsPage with DataTable, create modal, and row navigation,
and EnvironmentDetailPage with app list, inline rename, new app form with JAR
upload, and delete confirmation — all gated by RBAC permissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces placeholder DashboardPage with a full implementation: tenant
name + tier badge, KPI strip (environments, total apps, running, stopped),
environment list with per-env app counts, and a recent deployments
placeholder. Uses EnvApps helper component to fetch per-environment app
data without violating hook rules.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires up AppShell + Sidebar compound component, a per-environment
SidebarTree that lazy-fetches apps, React Router nested routes, and
provider-wrapped main.tsx with ThemeProvider/ToastProvider/BreadcrumbProvider.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>