#!/bin/sh # # Setup script for Zitadel OIDC application (dev environment only). # # Creates a "PVM" project and "PVM Dashboard" OIDC application in Zitadel. # Idempotent: skips creation if project/app already exist. # # Usage: # ./docker/setup-zitadel.sh # set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ZITADEL_URL="http://localhost:8080" PAT_FILE="${SCRIPT_DIR}/machinekey/admin.pat" DASHBOARD_ENV_FILE="${SCRIPT_DIR}/../apps/dashboard/.env" # ---------- helpers ---------- log() { printf '[setup-zitadel] %s\n' "$*" >&2; } fail() { log "ERROR: $*" >&2; exit 1; } # ---------- preflight ---------- command -v jq >/dev/null 2>&1 || fail "jq is required but not installed. Install it with: apt install jq" wait_for_zitadel() { log "Waiting for Zitadel to be healthy..." for _ in $(seq 1 30); do if curl -sf "${ZITADEL_URL}/debug/healthz" > /dev/null 2>&1; then log "Zitadel is healthy." return 0 fi sleep 2 done fail "Zitadel did not become healthy within 60 seconds." } wait_for_pat() { log "Waiting for PAT file at ${PAT_FILE}..." for _ in $(seq 1 15); do if [ -s "$PAT_FILE" ]; then log "PAT file found." return 0 fi sleep 2 done fail "PAT file not found or empty after 30 seconds. Did Zitadel finish init?" } # ---------- API calls ---------- find_project() { RESPONSE=$(curl -sf -X POST "${ZITADEL_URL}/management/v1/projects/_search" \ -H "Authorization: Bearer ${PAT}" \ -H "Content-Type: application/json" \ -d '{"queries":[{"nameQuery":{"name":"PVM","method":"TEXT_QUERY_METHOD_EQUALS"}}]}') || return 1 PROJECT_ID=$(printf '%s' "$RESPONSE" | jq -r '.result[0].id // empty') if [ -n "$PROJECT_ID" ]; then echo "$PROJECT_ID" return 0 fi return 1 } find_oidc_app() { PROJECT_ID="$1" RESPONSE=$(curl -sf "${ZITADEL_URL}/management/v1/projects/${PROJECT_ID}/apps/_search" \ -H "Authorization: Bearer ${PAT}" \ -H "Content-Type: application/json" \ -d '{}') || return 1 # Look for "PVM Dashboard" in the app list and extract its clientId CLIENT_ID=$(printf '%s' "$RESPONSE" | jq -r '.result[] | select(.name == "PVM Dashboard") | .oidcConfig.clientId // empty' 2>/dev/null) if [ -n "$CLIENT_ID" ]; then echo "$CLIENT_ID" return 0 fi return 1 } create_project() { log "Creating project 'PVM'..." RESPONSE=$(curl -sf -X POST "${ZITADEL_URL}/management/v1/projects" \ -H "Authorization: Bearer ${PAT}" \ -H "Content-Type: application/json" \ -d '{"name": "PVM"}') \ || fail "Failed to create project." PROJECT_ID=$(printf '%s' "$RESPONSE" | jq -r '.id // empty') [ -n "$PROJECT_ID" ] || fail "Could not extract project ID from response: $RESPONSE" log "Project created with ID: $PROJECT_ID" echo "$PROJECT_ID" } create_oidc_app() { PROJECT_ID="$1" # devMode=true skips redirect URI validation (allows http://). Disable in production. if [ "${ZITADEL_PRODUCTION:-}" = "true" ]; then DEV_MODE="false" else DEV_MODE="true" fi log "Creating OIDC application 'PVM Dashboard' in project $PROJECT_ID (devMode=$DEV_MODE)..." RESPONSE=$(curl -sf -X POST "${ZITADEL_URL}/management/v1/projects/${PROJECT_ID}/apps/oidc" \ -H "Authorization: Bearer ${PAT}" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"PVM Dashboard\", \"redirectUris\": [\"http://localhost:5173/auth/callback/zitadel\"], \"postLogoutRedirectUris\": [\"http://localhost:5173\", \"http://localhost:5173/login\"], \"responseTypes\": [\"OIDC_RESPONSE_TYPE_CODE\"], \"grantTypes\": [\"OIDC_GRANT_TYPE_AUTHORIZATION_CODE\", \"OIDC_GRANT_TYPE_REFRESH_TOKEN\"], \"appType\": \"OIDC_APP_TYPE_WEB\", \"authMethodType\": \"OIDC_AUTH_METHOD_TYPE_BASIC\", \"devMode\": ${DEV_MODE}, \"accessTokenType\": \"OIDC_TOKEN_TYPE_BEARER\", \"accessTokenRoleAssertion\": true, \"idTokenRoleAssertion\": true, \"idTokenUserinfoAssertion\": true }") \ || fail "Failed to create OIDC app." CLIENT_ID=$(printf '%s' "$RESPONSE" | jq -r '.clientId // empty') CLIENT_SECRET=$(printf '%s' "$RESPONSE" | jq -r '.clientSecret // empty') [ -n "$CLIENT_ID" ] || fail "Could not extract client ID from response: $RESPONSE" log "OIDC app created." log " Client ID: $CLIENT_ID" [ -n "$CLIENT_SECRET" ] && log " Client Secret: $CLIENT_SECRET" echo "${CLIENT_ID}|${CLIENT_SECRET}" } write_dashboard_env() { CLIENT_ID="$1" CLIENT_SECRET="$2" AUTH_SECRET=$(openssl rand -base64 32) log "Writing dashboard .env to ${DASHBOARD_ENV_FILE}..." cat > "$DASHBOARD_ENV_FILE" <