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 @@
+
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 @@
+