docs: update CLAUDE.md and HOWTO.md for all session changes
All checks were successful
CI / build (push) Successful in 1m5s
CI / docker (push) Successful in 9s

- Certificate management (provider interface, lifecycle, bootstrap, UI)
- Async tenant provisioning with polling UX
- Server restart capability (vendor + tenant)
- Audit log actor name resolution from Logto
- SSO connector management, vendor audit page
- Updated API reference with all current endpoints
- Fixed architecture table (per-tenant containers are dynamic)
- Updated migration list through V012

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-10 18:41:41 +02:00
parent 45bcc954ac
commit a48c4bfd08
2 changed files with 103 additions and 25 deletions

View File

@@ -33,17 +33,27 @@ Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. Th
- `TenantEntity.java` — JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB) - `TenantEntity.java` — JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB)
**vendor/** — Vendor console (platform:admin only) **vendor/** — Vendor console (platform:admin only)
- `VendorTenantService.java` — orchestrates tenant creation: DB record -> Logto org -> admin user -> license -> Docker provisioning -> OIDC config push -> redirect URI registration - `VendorTenantService.java` — orchestrates tenant creation (sync: DB + Logto + license, async: Docker provisioning + config push), suspend/activate, delete, restart server, license renewal
- `VendorTenantController.java` — REST at `/api/vendor/tenants` (platform:admin required) - `VendorTenantController.java` — REST at `/api/vendor/tenants` (platform:admin required)
**portal/** — Tenant admin portal (org-scoped) **portal/** — Tenant admin portal (org-scoped)
- `TenantPortalService.java` — customer-facing: dashboard (health from server), license, OIDC config, team, settings - `TenantPortalService.java` — customer-facing: dashboard (health from server), license, SSO connectors, team, settings, server restart
- `TenantPortalController.java` — REST at `/api/tenant/*` (org-scoped) - `TenantPortalController.java` — REST at `/api/tenant/*` (org-scoped)
**provisioning/** — Pluggable tenant provisioning **provisioning/** — Pluggable tenant provisioning
- `TenantProvisioner.java` — pluggable interface (like server's RuntimeOrchestrator) - `TenantProvisioner.java` — pluggable interface (like server's RuntimeOrchestrator)
- `DockerTenantProvisioner.java` — Docker implementation, creates per-tenant server + UI containers - `DockerTenantProvisioner.java` — Docker implementation, creates per-tenant server + UI containers
- `TenantProvisionerAutoConfig.java` — auto-detects Docker socket - `TenantProvisionerAutoConfig.java` — auto-detects Docker socket
- `DockerCertificateManager.java` — file-based cert management with atomic `.wip` swap (Docker volume)
- `DisabledCertificateManager.java` — no-op when certs dir unavailable
- `CertificateManagerAutoConfig.java` — auto-detects `/certs` directory
**certificate/** — TLS certificate lifecycle management
- `CertificateManager.java` — provider interface (Docker now, K8s later)
- `CertificateService.java` — orchestrates stage/activate/restore/discard, DB metadata, tenant CA staleness
- `CertificateController.java` — REST at `/api/vendor/certificates` (platform:admin required)
- `CertificateEntity.java` — JPA entity (status: ACTIVE/STAGED/ARCHIVED, subject, fingerprint, etc.)
- `CertificateStartupListener.java` — seeds DB from filesystem on boot (for bootstrap-generated certs)
**license/** — License management **license/** — License management
- `LicenseEntity.java` — JPA entity (id, tenant_id, tier, features JSONB, limits JSONB, expires_at) - `LicenseEntity.java` — JPA entity (id, tenant_id, tier, features JSONB, limits JSONB, expires_at)
@@ -52,26 +62,26 @@ Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. Th
**identity/** — Logto & server integration **identity/** — Logto & server integration
- `LogtoConfig.java` — Logto endpoint, M2M credentials (reads from bootstrap file) - `LogtoConfig.java` — Logto endpoint, M2M credentials (reads from bootstrap file)
- `LogtoManagementClient.java` — Logto Management API calls (create org, create user, add to org) - `LogtoManagementClient.java` — Logto Management API calls (create org, create user, add to org, get user, SSO connectors, JIT provisioning)
- `ServerApiClient.java` — M2M client for cameleer3-server API (Logto M2M token, `X-Cameleer-Protocol-Version: 1` header) - `ServerApiClient.java` — M2M client for cameleer3-server API (Logto M2M token, `X-Cameleer-Protocol-Version: 1` header)
**audit/** — Audit logging **audit/** — Audit logging
- `AuditEntity.java` — JPA entity (actor_id, tenant_id, action, resource, status) - `AuditEntity.java` — JPA entity (actor_id, actor_email, tenant_id, action, resource, status)
- `AuditService.java` — log audit events (TENANT_CREATE, TENANT_UPDATE, etc.) - `AuditService.java` — log audit events (TENANT_CREATE, TENANT_UPDATE, etc.); auto-resolves actor name from Logto when actorEmail is null (cached in-memory)
### React Frontend (`ui/src/`) ### React Frontend (`ui/src/`)
- `main.tsx` — React 19 root - `main.tsx` — React 19 root
- `router.tsx``/vendor/*` + `/tenant/*` with `RequireScope` guards and `LandingRedirect` that waits for scopes - `router.tsx``/vendor/*` + `/tenant/*` with `RequireScope` guards and `LandingRedirect` that waits for scopes
- `Layout.tsx` — persona-aware sidebar: vendor sees expandable "Vendor" section (Tenants + Logto link), tenant admin sees Dashboard/License/OIDC/Team/Settings - `Layout.tsx` — persona-aware sidebar: vendor sees expandable "Vendor" section (Tenants, Audit Log, Certificates, Identity/Logto), tenant admin sees Dashboard/License/SSO/Team/Audit/Settings
- `OrgResolver.tsx` — merges global + org-scoped token scopes (vendor's platform:admin is global) - `OrgResolver.tsx` — merges global + org-scoped token scopes (vendor's platform:admin is global)
- `config.ts` — fetch Logto config from /platform/api/config - `config.ts` — fetch Logto config from /platform/api/config
- `auth/useAuth.ts` — auth hook (isAuthenticated, logout, signIn) - `auth/useAuth.ts` — auth hook (isAuthenticated, logout, signIn)
- `auth/useOrganization.ts` — Zustand store for current tenant - `auth/useOrganization.ts` — Zustand store for current tenant
- `auth/useScopes.ts` — decode JWT scopes, hasScope() - `auth/useScopes.ts` — decode JWT scopes, hasScope()
- `auth/ProtectedRoute.tsx` — guard (redirects to /login) - `auth/ProtectedRoute.tsx` — guard (redirects to /login)
- **Vendor pages**: `VendorTenantsPage.tsx`, `CreateTenantPage.tsx`, `TenantDetailPage.tsx` - **Vendor pages**: `VendorTenantsPage.tsx`, `CreateTenantPage.tsx`, `TenantDetailPage.tsx`, `VendorAuditPage.tsx`, `CertificatesPage.tsx`
- **Tenant pages**: `TenantDashboardPage.tsx`, `TenantLicensePage.tsx`, `OidcConfigPage.tsx`, `TeamPage.tsx`, `SettingsPage.tsx` - **Tenant pages**: `TenantDashboardPage.tsx`, `TenantLicensePage.tsx`, `SsoPage.tsx`, `TeamPage.tsx`, `TenantAuditPage.tsx`, `SettingsPage.tsx`
### Custom Sign-in UI (`ui/sign-in/src/`) ### Custom Sign-in UI (`ui/sign-in/src/`)
@@ -97,7 +107,7 @@ All services on one hostname. Two env vars control everything: `PUBLIC_HOST` + `
- SPA assets at `/_app/` (Vite `assetsDir: '_app'`) to avoid conflict with Logto's `/assets/` - SPA assets at `/_app/` (Vite `assetsDir: '_app'`) to avoid conflict with Logto's `/assets/`
- Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain, same origin) - Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain, same origin)
- TLS: self-signed cert init container (`traefik-certs`) for dev, ACME for production - TLS: `traefik-certs` init container generates self-signed cert (dev) or copies user-supplied cert via `CERT_FILE`/`KEY_FILE`/`CA_FILE` env vars. Runtime cert replacement via vendor UI (stage/activate/restore). ACME for production (future).
- Root `/` -> `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`) - Root `/` -> `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`)
- LoginPage auto-redirects to Logto OIDC (no intermediate button) - LoginPage auto-redirects to Logto OIDC (no intermediate button)
- Per-tenant server containers get Traefik labels for `/t/{slug}/*` routing at provisioning time - Per-tenant server containers get Traefik labels for `/t/{slug}/*` routing at provisioning time
@@ -163,7 +173,7 @@ These env vars are injected into provisioned per-tenant server containers:
|---------|-------|---------| |---------|-------|---------|
| `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation | | `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation |
| `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch | | `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch |
| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev only) | | `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` (conditional) | Skip cert verify for OIDC discovery; only set when no `/certs/ca.pem` exists |
| `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik | | `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik |
| `CAMELEER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration | | `CAMELEER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration |
| `CAMELEER_SERVER_URL` | `http://cameleer3-server-{slug}:8081` | Per-tenant server URL (DNS alias on tenant network) | | `CAMELEER_SERVER_URL` | `http://cameleer3-server-{slug}:8081` | Per-tenant server URL (DNS alias on tenant network) |
@@ -181,6 +191,7 @@ These env vars are injected into provisioned per-tenant server containers:
|-------|---------------|---------| |-------|---------------|---------|
| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket for app deployment orchestration | | `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket for app deployment orchestration |
| `cameleer-jars-{slug}` (volume) | `/data/jars` | Shared JAR storage — server writes, deployed app containers read | | `cameleer-jars-{slug}` (volume) | `/data/jars` | Shared JAR storage — server writes, deployed app containers read |
| `cameleer-saas_certs` (volume, ro) | `/certs` | Platform TLS certs + CA bundle for OIDC trust |
### Server OIDC role extraction (two paths) ### Server OIDC role extraction (two paths)
@@ -228,23 +239,32 @@ Idempotent script run via `logto-bootstrap` init container. **Clean slate** —
12. (Optional) Vendor seed: create `saas-vendor` global role, vendor user, grant Logto console access (`VENDOR_SEED_ENABLED=true` in dev). 12. (Optional) Vendor seed: create `saas-vendor` global role, vendor user, grant Logto console access (`VENDOR_SEED_ENABLED=true` in dev).
The compose stack is: Traefik + PostgreSQL + ClickHouse + Logto + logto-bootstrap + cameleer-saas. The compose stack is: Traefik + PostgreSQL + ClickHouse + Logto + logto-bootstrap + cameleer-saas. No `cameleer3-server` or `cameleer3-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`. The compose stack is: Traefik + traefik-certs (init) + PostgreSQL + ClickHouse + Logto + logto-bootstrap (init) + cameleer-saas. No `cameleer3-server` or `cameleer3-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`.
### Tenant Provisioning Flow ### Tenant Provisioning Flow
When vendor creates a tenant via `VendorTenantService`: When vendor creates a tenant via `VendorTenantService`:
**Synchronous (in `createAndProvision`):**
1. Create `TenantEntity` (status=PROVISIONING) + Logto organization 1. Create `TenantEntity` (status=PROVISIONING) + Logto organization
2. Create admin user in Logto with owner org role 2. Create admin user in Logto with owner org role
3. Add vendor user to new org for support access 3. Add vendor user to new org for support access
4. Register OIDC redirect URIs for `/t/{slug}/oidc/callback` on Logto Traditional Web App 4. Register OIDC redirect URIs for `/t/{slug}/oidc/callback` on Logto Traditional Web App
5. Generate license (tier-appropriate, 365 days) 5. Generate license (tier-appropriate, 365 days)
6. Create tenant-isolated Docker network (`cameleer-tenant-{slug}`) 6. Return immediately — UI shows provisioning spinner, polls via `refetchInterval`
7. Create server container with env vars, Traefik labels (`traefik.docker.network`), health check, Docker socket bind, JAR volume (`cameleer-jars-{slug}:/data/jars`)
8. Create UI container with `CAMELEER_API_URL`, `BASE_PATH`, Traefik strip-prefix labels **Asynchronous (in `provisionAsync`, `@Async`):**
9. Wait for health check (`/api/v1/health`, not `/actuator/health` which requires auth) 7. Create tenant-isolated Docker network (`cameleer-tenant-{slug}`)
10. Push license token to server via M2M API 8. Create server container with env vars, Traefik labels (`traefik.docker.network`), health check, Docker socket bind, JAR volume, certs volume (ro)
11. Push OIDC config (Traditional Web App credentials + `additionalScopes: [urn:logto:scope:organizations, urn:logto:scope:organization_roles]`) to server for SSO 9. Create UI container with `CAMELEER_API_URL`, `BASE_PATH`, Traefik strip-prefix labels
11. Update tenant status -> ACTIVE 10. Wait for health check (`/api/v1/health`, not `/actuator/health` which requires auth)
11. Push license token to server via M2M API
12. Push OIDC config (Traditional Web App credentials + `additionalScopes: [urn:logto:scope:organizations, urn:logto:scope:organization_roles]`) to server for SSO
13. Update tenant status -> ACTIVE (or set `provisionError` on failure)
**Server restart** (available to vendor + tenant admin):
- `POST /api/vendor/tenants/{id}/restart` (vendor) and `POST /api/tenant/server/restart` (tenant)
- Calls `TenantProvisioner.stop(slug)` then `start(slug)` — restarts server + UI containers only
## Database Migrations ## Database Migrations
@@ -258,6 +278,8 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/`
- V007 — audit_log - V007 — audit_log
- V008 — app resource limits - V008 — app resource limits
- V010 — cleanup of migrated tables - V010 — cleanup of migrated tables
- V011 — add provisioning fields (server_endpoint, provision_error)
- V012 — certificates table + tenants.ca_applied_at
## Related Conventions ## Related Conventions
@@ -280,7 +302,7 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/`
<!-- gitnexus:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # GitNexus — Code Intelligence
This project is indexed by GitNexus as **cameleer-saas** (1976 symbols, 3805 relationships, 165 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. This project is indexed by GitNexus as **cameleer-saas** (2057 symbols, 4069 relationships, 172 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

View File

@@ -35,19 +35,21 @@ curl http://localhost:8080/actuator/health
## Architecture ## Architecture
The platform runs as a Docker Compose stack with 6 services: The platform runs as a Docker Compose stack:
| Service | Image | Port | Purpose | | Service | Image | Port | Purpose |
|---------|-------|------|---------| |---------|-------|------|---------|
| **traefik** | traefik:v3 | 80, 443 | Reverse proxy, TLS, routing | | **traefik-certs** | alpine:latest | — | Init container: generates self-signed cert or copies user-supplied cert |
| **traefik** | traefik:v3 | 80, 443, 3002 | Reverse proxy, TLS termination, routing |
| **postgres** | postgres:16-alpine | 5432* | Platform database + Logto database | | **postgres** | postgres:16-alpine | 5432* | Platform database + Logto database |
| **logto** | ghcr.io/logto-io/logto | 3001*, 3002* | Identity provider (OIDC) | | **logto** | ghcr.io/logto-io/logto | 3001*, 3002* | Identity provider (OIDC) |
| **cameleer-saas** | cameleer-saas:latest | 8080* | SaaS API server | | **cameleer-saas** | cameleer-saas:latest | 8080* | SaaS API server + vendor UI |
| **cameleer3-server** | cameleer3-server:latest | 8081 | Observability backend |
| **clickhouse** | clickhouse-server:latest | 8123* | Trace/metrics/log storage | | **clickhouse** | clickhouse-server:latest | 8123* | Trace/metrics/log storage |
*Ports exposed to host only with `docker-compose.dev.yml` overlay. *Ports exposed to host only with `docker-compose.dev.yml` overlay.
Per-tenant `cameleer3-server` and `cameleer3-server-ui` containers are provisioned dynamically by `DockerTenantProvisioner` — they are NOT part of the compose stack.
## Installation ## Installation
### 1. Environment Configuration ### 1. Environment Configuration
@@ -83,7 +85,25 @@ This creates `keys/ed25519.key` (private) and `keys/ed25519.pub` (public). The k
If no key files are configured, the platform generates ephemeral keys on startup (suitable for development only -- keys change on every restart). If no key files are configured, the platform generates ephemeral keys on startup (suitable for development only -- keys change on every restart).
### 3. Start the Stack ### 3. TLS Certificate (Optional)
By default, the `traefik-certs` init container generates a self-signed certificate for `PUBLIC_HOST`. To supply your own certificate at bootstrap time, set these env vars in `.env`:
```bash
CERT_FILE=/path/to/cert.pem # PEM-encoded certificate
KEY_FILE=/path/to/key.pem # PEM-encoded private key
CA_FILE=/path/to/ca.pem # Optional: CA bundle (for private CA trust)
```
The init container validates that the key matches the certificate before accepting. If validation fails, the container exits with an error.
**Runtime certificate replacement** is available via the vendor UI at `/vendor/certificates`:
- Upload a new cert+key+CA bundle (staged, not yet active)
- Validate and activate (atomic swap, Traefik hot-reloads)
- Roll back to the previous certificate if needed
- Track which tenants need a restart to pick up CA bundle changes
### 4. Start the Stack
**Development** (ports exposed for direct access): **Development** (ports exposed for direct access):
```bash ```bash
@@ -95,7 +115,7 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
docker compose up -d docker compose up -d
``` ```
### 4. Verify Services ### 5. Verify Services
```bash ```bash
# Health check # Health check
@@ -287,6 +307,42 @@ Query params: `since`, `until` (ISO timestamps), `limit` (default 500), `stream`
|------|-------------| |------|-------------|
| `/dashboard` | cameleer3-server observability dashboard (forward-auth protected) | | `/dashboard` | cameleer3-server observability dashboard (forward-auth protected) |
### Vendor: Certificates (platform:admin)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/vendor/certificates` | Overview (active, staged, archived, stale count) |
| POST | `/api/vendor/certificates/stage` | Upload cert+key+CA (multipart) |
| POST | `/api/vendor/certificates/activate` | Promote staged -> active |
| POST | `/api/vendor/certificates/restore` | Swap archived <-> active |
| DELETE | `/api/vendor/certificates/staged` | Discard staged cert |
| GET | `/api/vendor/certificates/stale-tenants` | Count tenants needing CA restart |
### Vendor: Tenants (platform:admin)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/vendor/tenants` | List all tenants |
| POST | `/api/vendor/tenants` | Create tenant (async provisioning) |
| GET | `/api/vendor/tenants/{id}` | Tenant detail + server state |
| POST | `/api/vendor/tenants/{id}/restart` | Restart server containers |
| POST | `/api/vendor/tenants/{id}/suspend` | Suspend tenant |
| POST | `/api/vendor/tenants/{id}/activate` | Activate tenant |
| DELETE | `/api/vendor/tenants/{id}` | Delete tenant |
| POST | `/api/vendor/tenants/{id}/license` | Renew license |
### Tenant Portal (org-scoped)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/tenant/dashboard` | Tenant dashboard data |
| GET | `/api/tenant/license` | License details |
| POST | `/api/tenant/server/restart` | Restart server |
| GET | `/api/tenant/team` | List team members |
| POST | `/api/tenant/team/invite` | Invite team member |
| DELETE | `/api/tenant/team/{userId}` | Remove team member |
| GET | `/api/tenant/settings` | Tenant settings |
| GET | `/api/tenant/sso` | List SSO connectors |
| POST | `/api/tenant/sso` | Create SSO connector |
| GET | `/api/tenant/audit` | Tenant audit log |
### Health ### Health
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|