feat: use design system brand icons for favicon, login, sidebar
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled

Replace hand-crafted favicon.svg with official brand assets from
@cameleer/design-system v0.1.32: PNG favicons (16/32px) and
camel-logo.svg for login dialog and sidebar. Update SecurityConfig
public endpoints accordingly. Update documentation for architecture
cleanup (PKCE, OidcProviderHelper, role normalization, K8s hardening,
Dockerfile credential removal, CI deduplication, sidebar path fix).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-06 22:08:58 +02:00
parent 0de392ff6e
commit 0761d0dbee
12 changed files with 32 additions and 16 deletions

View File

@@ -41,8 +41,8 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
- Multi-tenancy: each server instance serves one tenant (configured via `CAMELEER_TENANT_ID`, default: `"default"`). Environments (dev/staging/prod) are first-class — agents send `environmentId` at registration and in heartbeats. JWT carries `env` claim for environment persistence across token refresh. PostgreSQL isolated via schema-per-tenant (`?currentSchema=tenant_{id}`). ClickHouse shared DB with `tenant_id` + `environment` columns, partitioned by `(tenant_id, toYYYYMM(timestamp))`.
- Storage: PostgreSQL for RBAC, config, and audit; ClickHouse for all observability data (executions, search, logs, metrics, stats, diagrams). ClickHouse schema migrations in `clickhouse/*.sql`, run idempotently on startup by `ClickHouseSchemaInitializer`. Use `IF NOT EXISTS` for CREATE and ADD PROJECTION.
- Logging: ClickHouse JDBC set to INFO (`com.clickhouse`), HTTP client to WARN (`org.apache.hc.client5`) in application.yml
- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing (key derived deterministically from JWT secret via HMAC-SHA256), bootstrap token for registration. CORS: `CAMELEER_CORS_ALLOWED_ORIGINS` (comma-separated) overrides `CAMELEER_UI_ORIGIN` for multi-origin setups (e.g., reverse proxy). UI role gating: Admin sidebar/routes hidden for non-ADMIN; diagram toolbar and route control hidden for VIEWER; Config is a main tab (`/config` all apps, `/config/:appId` single app with detail; sidebar clicks stay on config, route clicks resolve to parent app). Read-only for VIEWER, editable for OPERATOR+. Role helpers: `useIsAdmin()`, `useCanControl()` in `auth-store.ts`. Route guard: `RequireAdmin` in `auth/RequireAdmin.tsx`.
- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API/UI, stored in database (`server_config` table). Configurable `userIdClaim` (default `sub`) determines which id_token claim is used as the user identifier. Resource server mode: accepts external access tokens (Logto M2M) via JWKS validation when `CAMELEER_OIDC_ISSUER_URI` is set. `CAMELEER_OIDC_JWK_SET_URI` overrides JWKS discovery for container networking. `CAMELEER_OIDC_TLS_SKIP_VERIFY=true` disables TLS cert verification for OIDC calls (self-signed CAs). Scope-based role mapping (case-insensitive): `admin`/`server:admin` → ADMIN, `operator`/`server:operator` → OPERATOR, `viewer`/`server:viewer` → VIEWER. SSO: when OIDC enabled, UI auto-redirects to provider with `prompt=none` for silent sign-in; falls back to `/login?local` on `login_required`, retries without `prompt=none` on `consent_required`. Auto-signup provisions new OIDC users with default roles. System roles synced on every OIDC login (revocations propagate on next login); group memberships are never touched. Supports ES384, ES256, RS256.
- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing (key derived deterministically from JWT secret via HMAC-SHA256), bootstrap token for registration. CORS: `CAMELEER_CORS_ALLOWED_ORIGINS` (comma-separated) overrides `CAMELEER_UI_ORIGIN` for multi-origin setups (e.g., reverse proxy). UI role gating: Admin sidebar/routes hidden for non-ADMIN; diagram toolbar and route control hidden for VIEWER; Config is a main tab (`/config` all apps, `/config/:appId` single app with detail; sidebar clicks stay on config, route clicks resolve to parent app). Read-only for VIEWER, editable for OPERATOR+. Role helpers: `useIsAdmin()`, `useCanControl()` in `auth-store.ts`. Route guard: `RequireAdmin` in `auth/RequireAdmin.tsx`. PKCE (S256) enabled on all OIDC authorization requests.
- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API/UI, stored in database (`server_config` table). Configurable `userIdClaim` (default `sub`) determines which id_token claim is used as the user identifier. Resource server mode: accepts external access tokens (Logto M2M) via JWKS validation when `CAMELEER_OIDC_ISSUER_URI` is set. `CAMELEER_OIDC_JWK_SET_URI` overrides JWKS discovery for container networking. `CAMELEER_OIDC_TLS_SKIP_VERIFY=true` disables TLS cert verification for OIDC calls (self-signed CAs). Scope-based role mapping via `SystemRole.normalizeScope()` (case-insensitive, strips `server:` prefix): `admin`/`server:admin` → ADMIN, `operator`/`server:operator` → OPERATOR, `viewer`/`server:viewer` → VIEWER. SSO: when OIDC enabled, UI auto-redirects to provider with `prompt=none` + PKCE (S256) for silent sign-in; falls back to `/login?local` on `login_required`, retries without `prompt=none` on `consent_required`. Logout always redirects to `/login?local` (via OIDC end_session or direct fallback) to prevent SSO re-login loops. Auto-signup provisions new OIDC users with default roles. System roles synced on every OIDC login (revocations propagate on next login); group memberships are never touched. Supports ES384, ES256, RS256. Shared OIDC logic in `OidcProviderHelper` (discovery, JWK source, algorithm set).
- User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users`
- Usage analytics: ClickHouse `usage_events` table tracks authenticated UI requests, flushed every 5s
@@ -57,12 +57,17 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
- Deployment target: k3s at 192.168.50.86, namespace `cameleer` (main), `cam-<slug>` (feature branches)
- Feature branches: isolated namespace, PG schema; Traefik Ingress at `<slug>-api.cameleer.siegeln.net`
- Secrets managed in CI deploy step (idempotent `--dry-run=client | kubectl apply`): `cameleer-auth`, `postgres-credentials`, `clickhouse-credentials`
- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready`
- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready -U "$POSTGRES_USER"` (env var, not hardcoded)
- K8s security: server and database pods run with `securityContext.runAsNonRoot`. UI (nginx) runs without securityContext (needs root for entrypoint setup).
- Docker: server Dockerfile has no default credentials — all DB config comes from env vars at runtime
- Docker build uses buildx registry cache + `--provenance=false` for Gitea compatibility
- CI: branch slug sanitization extracted to `.gitea/sanitize-branch.sh`, sourced by docker and deploy-feature jobs
## UI Styling
- Always use `@cameleer/design-system` CSS variables for colors (`var(--amber)`, `var(--error)`, `var(--success)`, etc.) — never hardcode hex values. This applies to CSS modules, inline styles, and SVG `fill`/`stroke` attributes. SVG presentation attributes resolve `var()` correctly.
- Brand assets: `@cameleer/design-system/assets/` provides `camel-logo.svg` (currentColor), `cameleer3-{16,32,48,192,512}.png`, and `cameleer3-logo.png`. Copied to `ui/public/` for use as favicon (`favicon-16.png`, `favicon-32.png`) and logo (`camel-logo.svg` — login dialog 36px, sidebar 28x24px).
- Sidebar generates `/exchanges/` paths directly (no legacy `/apps/` redirects). basePath is centralized in `ui/src/config.ts`; router.tsx imports it instead of re-reading `<base>` tag.
- Global user preferences (environment selection) use Zustand stores with localStorage persistence — never URL search params. URL params are for page-specific state only (e.g. `?text=` search query). Switching environment resets all filters and remounts pages.
## Disabled Skills

View File

@@ -39,9 +39,15 @@ PostgreSQL credentials: `cameleer` / `cameleer_dev`, database `cameleer3`.
```bash
mvn clean package -DskipTests
CAMELEER_AUTH_TOKEN=my-secret-token java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/cameleer3 \
SPRING_DATASOURCE_USERNAME=cameleer \
SPRING_DATASOURCE_PASSWORD=cameleer_dev \
CAMELEER_AUTH_TOKEN=my-secret-token \
java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
```
> **Note:** The Docker image no longer includes default database credentials. When running via `docker run`, pass `-e SPRING_DATASOURCE_URL=...` etc. The docker-compose setup provides these automatically.
The server starts on **port 8081**. The `CAMELEER_AUTH_TOKEN` environment variable is **required** — the server fails fast on startup if it is not set.
For token rotation without downtime, set `CAMELEER_AUTH_TOKEN_PREVIOUS` to the old token while rolling out the new one. The server accepts both during the overlap window.

View File

@@ -94,7 +94,8 @@ public class SecurityConfig {
"/",
"/index.html",
"/config.js",
"/favicon.svg",
"/favicon-*.png",
"/camel-logo.svg",
"/assets/**"
).permitAll()

View File

@@ -271,11 +271,11 @@ Server derives an Ed25519 keypair deterministically from the JWT secret. Public
### OIDC Integration
Configured via admin API (`/api/v1/admin/oidc`) or admin UI. Supports any OpenID Connect provider. Features: configurable user ID claim (`userIdClaim`, default `sub` — e.g., `email`, `preferred_username`), role claim extraction (supports nested paths like `realm_access.roles`), auto-signup (auto-provisions new users on first OIDC login), configurable display name claim, constant-time token rotation via dual bootstrap tokens. Supports ES384 (Logto default), ES256, and RS256 for id_token validation. System roles are synced on every OIDC login (not just first) — revoking a scope in the provider takes effect on next login. Group memberships (manually assigned) are never touched by the sync.
Configured via admin API (`/api/v1/admin/oidc`) or admin UI. Supports any OpenID Connect provider. Features: configurable user ID claim (`userIdClaim`, default `sub` — e.g., `email`, `preferred_username`), role claim extraction (supports nested paths like `realm_access.roles`), auto-signup (auto-provisions new users on first OIDC login), configurable display name claim, constant-time token rotation via dual bootstrap tokens, PKCE (S256) on all authorization requests. Supports ES384 (Logto default), ES256, and RS256 for id_token validation. System roles are synced on every OIDC login (not just first) — revoking a scope in the provider takes effect on next login. Group memberships (manually assigned) are never touched by the sync. Role normalization via `SystemRole.normalizeScope()` (case-insensitive, strips `server:` prefix). Shared OIDC infrastructure (discovery, JWK source, algorithm set) centralized in `OidcProviderHelper`.
### SSO Auto-Redirect
When OIDC is configured and enabled, the login page automatically redirects to the OIDC provider with `prompt=none` for silent SSO. If the user has an active provider session, they are signed in without seeing a login form. If `consent_required` is returned (first login, scopes not yet granted), the flow retries without `prompt=none` so the user can grant consent once. If `login_required` (no provider session), falls back to the login form. Bypass auto-redirect with `/login?local`.
When OIDC is configured and enabled, the login page automatically redirects to the OIDC provider with `prompt=none` and PKCE (S256) for silent SSO. If the user has an active provider session, they are signed in without seeing a login form. If `consent_required` is returned (first login, scopes not yet granted), the flow retries without `prompt=none` so the user can grant consent once. If `login_required` (no provider session), falls back to the login form. Bypass auto-redirect with `/login?local`. Logout always redirects to `/login?local` — either via the OIDC `end_session_endpoint` (with `post_logout_redirect_uri`) or as a direct fallback — preventing SSO re-login loops.
### OIDC Resource Server
@@ -392,7 +392,7 @@ Stats tables are fed by Materialized Views from base tables. Query with `-Merge(
### Container Image
Multi-stage Docker build: Maven 3.9 + JDK 17 (build) → JRE 17 (runtime). Port 8081.
Multi-stage Docker build: Maven 3.9 + JDK 17 (build) → JRE 17 (runtime). Port 8081. No default credentials baked in — all database config comes from env vars at runtime.
Registry: `gitea.siegeln.net/cameleer/cameleer3-server`

View File

@@ -2,7 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cameleer3</title>
</head>

8
ui/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "ui",
"version": "0.0.0",
"dependencies": {
"@cameleer/design-system": "^0.1.31",
"@cameleer/design-system": "^0.1.32",
"@tanstack/react-query": "^5.90.21",
"lucide-react": "^1.7.0",
"openapi-fetch": "^0.17.0",
@@ -278,9 +278,9 @@
}
},
"node_modules/@cameleer/design-system": {
"version": "0.1.31",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.31/design-system-0.1.31.tgz",
"integrity": "sha512-yfuMQdLB3RS6loT31YozKCyBUnctxlLPZjpzPzD7UQZhCFU4+ScibMzbPeLFE/MyMkfrpk/j9v8pJCHW7l64TQ==",
"version": "0.1.32",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.32/design-system-0.1.32.tgz",
"integrity": "sha512-ocbTUTUZEpDAprsYD3Ao2ZVZXGSbFg3amL8FS85FoJKdJ3xKZO/kpkTKPl+qmsbh/E030QwdulBR8YnQYlHDBg==",
"dependencies": {
"lucide-react": "^1.7.0",
"react": "^19.0.0",

View File

@@ -14,7 +14,7 @@
"generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
},
"dependencies": {
"@cameleer/design-system": "^0.1.31",
"@cameleer/design-system": "^0.1.32",
"@tanstack/react-query": "^5.90.21",
"lucide-react": "^1.7.0",
"openapi-fetch": "^0.17.0",

3
ui/public/camel-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
ui/public/favicon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

BIN
ui/public/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -131,7 +131,7 @@ export function LoginPage() {
<Card className={styles.card}>
<div className={styles.loginForm}>
<div className={styles.logo}>
<img src={`${config.basePath}favicon.svg`} alt="" className={styles.logoImg} />
<img src={`${config.basePath}camel-logo.svg`} alt="" className={styles.logoImg} />
cameleer3
</div>
<p className={styles.subtitle}>{subtitle}</p>

View File

@@ -625,7 +625,7 @@ function LayoutContent() {
// --- Render -------------------------------------------------------
const camelLogo = (
<img src={`${config.basePath}favicon.svg`} alt="" style={{ width: 28, height: 24 }} />
<img src={`${config.basePath}camel-logo.svg`} alt="" style={{ width: 28, height: 24 }} />
);
const sidebarElement = (