diff --git a/docker/logto-bootstrap.sh b/docker/logto-bootstrap.sh index 0c6ec0a..3a35619 100644 --- a/docker/logto-bootstrap.sh +++ b/docker/logto-bootstrap.sh @@ -130,6 +130,10 @@ api_put() { api_delete() { curl -s -X DELETE -H "Authorization: Bearer $TOKEN" -H "Host: ${HOST}" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true } +api_patch() { + curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -H "Host: ${HOST}" \ + -d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true +} # ============================================================ # PHASE 3: Create Logto applications @@ -232,10 +236,15 @@ SCOPE_OBSERVE_READ=$(create_scope "observe:read" "View observability data") SCOPE_OBSERVE_DEBUG=$(create_scope "observe:debug" "Debug and replay operations") SCOPE_SETTINGS_MANAGE=$(create_scope "settings:manage" "Manage settings") +# Server-level scopes (mapped to server RBAC roles via JWT scope claim) +SCOPE_SERVER_ADMIN=$(create_scope "server:admin" "Full server access") +SCOPE_SERVER_OPERATOR=$(create_scope "server:operator" "Deploy and manage apps in server") +SCOPE_SERVER_VIEWER=$(create_scope "server:viewer" "Read-only server observability") + # Collect scope IDs for role assignment -ALL_TENANT_SCOPE_IDS="\"$SCOPE_TENANT_MANAGE\",\"$SCOPE_BILLING_MANAGE\",\"$SCOPE_TEAM_MANAGE\",\"$SCOPE_APPS_MANAGE\",\"$SCOPE_APPS_DEPLOY\",\"$SCOPE_SECRETS_MANAGE\",\"$SCOPE_OBSERVE_READ\",\"$SCOPE_OBSERVE_DEBUG\",\"$SCOPE_SETTINGS_MANAGE\"" +ALL_TENANT_SCOPE_IDS="\"$SCOPE_TENANT_MANAGE\",\"$SCOPE_BILLING_MANAGE\",\"$SCOPE_TEAM_MANAGE\",\"$SCOPE_APPS_MANAGE\",\"$SCOPE_APPS_DEPLOY\",\"$SCOPE_SECRETS_MANAGE\",\"$SCOPE_OBSERVE_READ\",\"$SCOPE_OBSERVE_DEBUG\",\"$SCOPE_SETTINGS_MANAGE\",\"$SCOPE_SERVER_ADMIN\"" ALL_SCOPE_IDS="\"$SCOPE_PLATFORM_ADMIN\",$ALL_TENANT_SCOPE_IDS" -MEMBER_SCOPE_IDS="\"$SCOPE_APPS_DEPLOY\",\"$SCOPE_OBSERVE_READ\",\"$SCOPE_OBSERVE_DEBUG\"" +MEMBER_SCOPE_IDS="\"$SCOPE_APPS_DEPLOY\",\"$SCOPE_OBSERVE_READ\",\"$SCOPE_OBSERVE_DEBUG\",\"$SCOPE_SERVER_VIEWER\"" # --- M2M app --- M2M_ID=$(echo "$EXISTING_APPS" | jq -r ".[] | select(.name == \"$M2M_APP_NAME\" and .type == \"MachineToMachine\") | .id") @@ -367,6 +376,16 @@ else fi fi +# --- Grant SaaS admin Logto console access --- +log "Granting SaaS admin Logto console access..." +ADMIN_MGMT_ROLE_ID=$(api_get "/api/roles" | jq -r '.[] | select(.name == "admin:admin") | .id') +if [ -n "$ADMIN_MGMT_ROLE_ID" ] && [ "$ADMIN_MGMT_ROLE_ID" != "null" ]; then + api_post "/api/users/$ADMIN_USER_ID/roles" "{\"roleIds\": [\"$ADMIN_MGMT_ROLE_ID\"]}" >/dev/null 2>&1 + log "SaaS admin granted Logto console access." +else + log "WARNING: admin:admin role not found — Logto console access not granted" +fi + # --- Tenant Admin --- log "Checking for tenant admin '$TENANT_ADMIN_USER'..." TENANT_USER_ID=$(api_get "/api/users?search=$TENANT_ADMIN_USER" | jq -r ".[] | select(.username == \"$TENANT_ADMIN_USER\") | .id") @@ -453,7 +472,9 @@ if [ "$SERVER_HEALTHY" = "yes" ] && [ -n "$TRAD_SECRET" ]; then \"clientSecret\": \"$TRAD_SECRET\", \"autoSignup\": true, \"defaultRoles\": [\"VIEWER\"], - \"displayNameClaim\": \"name\" + \"displayNameClaim\": \"name\", + \"rolesClaim\": \"scope\", + \"audience\": \"$API_RESOURCE_INDICATOR\" }") log "OIDC config response: $(echo "$OIDC_RESPONSE" | head -c 200)" log "cameleer3-server OIDC configured." @@ -464,6 +485,24 @@ else log "WARNING: cameleer3-server not available or no Traditional app secret — skipping OIDC config" fi +# ============================================================ +# PHASE 8: Configure sign-in branding +# ============================================================ + +log "Configuring sign-in experience branding..." +api_patch "/api/sign-in-exp" "{ + \"color\": { + \"primaryColor\": \"#C6820E\", + \"isDarkModeEnabled\": true, + \"darkPrimaryColor\": \"#D4941E\" + }, + \"branding\": { + \"logoUrl\": \"${PROTO}://${HOST}/platform/logo.svg\", + \"darkLogoUrl\": \"${PROTO}://${HOST}/platform/logo-dark.svg\" + } +}" +log "Sign-in branding configured." + # ============================================================ # PHASE 9: Cleanup seeded apps # ============================================================ diff --git a/src/main/java/net/siegeln/cameleer/saas/config/PublicConfigController.java b/src/main/java/net/siegeln/cameleer/saas/config/PublicConfigController.java index bb83e16..5955d90 100644 --- a/src/main/java/net/siegeln/cameleer/saas/config/PublicConfigController.java +++ b/src/main/java/net/siegeln/cameleer/saas/config/PublicConfigController.java @@ -36,7 +36,10 @@ public class PublicConfigController { "secrets:manage", "observe:read", "observe:debug", - "settings:manage" + "settings:manage", + "server:admin", + "server:operator", + "server:viewer" ); @GetMapping("/api/config") diff --git a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java index 0f56a4b..a168fdb 100644 --- a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java +++ b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java @@ -40,7 +40,7 @@ public class SecurityConfig { .requestMatchers("/api/config").permitAll() .requestMatchers("/", "/index.html", "/login", "/callback", "/environments/**", "/license", "/admin/**").permitAll() - .requestMatchers("/_app/**", "/favicon.ico").permitAll() + .requestMatchers("/_app/**", "/favicon.ico", "/logo.svg", "/logo-dark.svg").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> diff --git a/ui/public/logo-dark.svg b/ui/public/logo-dark.svg new file mode 100644 index 0000000..b1511a3 --- /dev/null +++ b/ui/public/logo-dark.svg @@ -0,0 +1,4 @@ + + + cameleer3 + diff --git a/ui/public/logo.svg b/ui/public/logo.svg new file mode 100644 index 0000000..a859971 --- /dev/null +++ b/ui/public/logo.svg @@ -0,0 +1,4 @@ + + + cameleer3 +