Design Prot Merge #1
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
# Shared helpers for the production deploy scripts.
|
||||
|
||||
step() { printf '\n==> %s\n' "$1"; }
|
||||
fail() { printf 'error: %s\n' "$1" >&2; exit 1; }
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "$1 is required but not installed."
|
||||
}
|
||||
|
||||
# env_get <file> <key> — prints the value, empty if the key is absent.
|
||||
env_get() {
|
||||
grep -E "^$2=" "$1" 2>/dev/null | tail -n 1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
# env_set <file> <key> <value> — replaces the key in place, appends if missing.
|
||||
env_set() {
|
||||
_file=$1; _key=$2; _value=$3
|
||||
_tmp=$(mktemp)
|
||||
if grep -qE "^$_key=" "$_file"; then
|
||||
awk -v k="$_key" -v v="$_value" '
|
||||
index($0, k "=") == 1 { print k "=" v; next }
|
||||
{ print }
|
||||
' "$_file" >"$_tmp"
|
||||
else
|
||||
cp "$_file" "$_tmp"
|
||||
printf '%s=%s\n' "$_key" "$_value" >>"$_tmp"
|
||||
fi
|
||||
cat "$_tmp" >"$_file"
|
||||
rm -f "$_tmp"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Applies EF Core migrations to the prod Postgres, from a throwaway SDK
|
||||
# container attached to the compose network (postgres is not published to the host).
|
||||
set -eu
|
||||
|
||||
DEPLOY_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
BACKEND_DIR="$(CDPATH= cd -- "$DEPLOY_DIR/../backend" && pwd)"
|
||||
ENV_FILE="$DEPLOY_DIR/.env.prod"
|
||||
|
||||
. "$ENV_FILE"
|
||||
|
||||
NETWORK="musify-prod_default"
|
||||
CONN="Host=postgres;Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
|
||||
|
||||
docker run --rm \
|
||||
--network "$NETWORK" \
|
||||
-v "$BACKEND_DIR":/src \
|
||||
-w /src/Musify.Infrastructure \
|
||||
mcr.microsoft.com/dotnet/sdk:10.0 \
|
||||
sh -c "dotnet tool install --global dotnet-ef >/dev/null 2>&1 || true; \
|
||||
export PATH=\$PATH:/root/.dotnet/tools; \
|
||||
dotnet restore Musify.Infrastructure.csproj; \
|
||||
dotnet user-secrets set 'Database:ConnectionString' '$CONN' >/dev/null; \
|
||||
dotnet ef database update --context Database"
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Applies the EF Core migrations to the production database from a throwaway SDK
|
||||
# container attached to the compose network (Postgres is not published to the host).
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
DEPLOY_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
BACKEND_DIR=$(CDPATH= cd -- "$DEPLOY_DIR/../backend" && pwd)
|
||||
ENV_FILE="$DEPLOY_DIR/.env.prod"
|
||||
NETWORK=${MUSIFY_NETWORK:-musify-prod_default}
|
||||
|
||||
. "$SCRIPT_DIR/lib.sh"
|
||||
|
||||
CONN="Host=postgres;Port=5432;Database=$(env_get "$ENV_FILE" POSTGRES_DB);Username=$(env_get "$ENV_FILE" POSTGRES_USER);Password=$(env_get "$ENV_FILE" POSTGRES_PASSWORD)"
|
||||
|
||||
# Musify.Infrastructure is both the migrations and the startup project: it owns
|
||||
# the IDesignTimeDbContextFactory and the EFCore.Design package.
|
||||
docker run --rm \
|
||||
--network "$NETWORK" \
|
||||
-v "$BACKEND_DIR":/src \
|
||||
-w /src/Musify.Infrastructure \
|
||||
-e CONN="$CONN" \
|
||||
mcr.microsoft.com/dotnet/sdk:10.0 \
|
||||
sh -c 'dotnet tool install --global dotnet-ef >/dev/null 2>&1 || true
|
||||
export PATH=$PATH:/root/.dotnet/tools
|
||||
dotnet restore Musify.Infrastructure.csproj
|
||||
dotnet user-secrets set "Database:ConnectionString" "$CONN" >/dev/null
|
||||
dotnet ef database update --context Database'
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Generates the RS256 key pair used to sign (API) and verify (Gateway) stream
|
||||
# tickets. No-op if both keys already exist.
|
||||
set -eu
|
||||
|
||||
KEYS_DIR=${1:?usage: stream-keys.sh <keys-dir>}
|
||||
PRIVATE_KEY="$KEYS_DIR/stream_private.pem"
|
||||
PUBLIC_KEY="$KEYS_DIR/stream_public.pem"
|
||||
|
||||
if [ -f "$PRIVATE_KEY" ] && [ -f "$PUBLIC_KEY" ]; then
|
||||
echo "Stream-ticket keys already present in $KEYS_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$KEYS_DIR"
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$PRIVATE_KEY" 2>/dev/null
|
||||
openssl rsa -in "$PRIVATE_KEY" -pubout -out "$PUBLIC_KEY" 2>/dev/null
|
||||
chmod 644 "$PUBLIC_KEY"
|
||||
chmod 600 "$PRIVATE_KEY"
|
||||
|
||||
echo "Generated stream-ticket keys in $KEYS_DIR"
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#!/bin/sh
|
||||
# Brings up (or updates) the Musify production stack on the server.
|
||||
#
|
||||
# ./up.sh build + start everything, provision Zitadel, migrate
|
||||
# ./up.sh --no-build same, reusing the images that are already built
|
||||
# ./up.sh --down stop and remove the containers (volumes are kept)
|
||||
#
|
||||
# Everything it needs lives in deploy/.env.prod (see .env.prod.example) and in
|
||||
# deploy/nginx/certs/ (origin.pem + origin.key).
|
||||
set -eu
|
||||
|
||||
DEPLOY_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ENV_FILE="$DEPLOY_DIR/.env.prod"
|
||||
INFRA="postgres zitadel rabbitmq seaweedfs seaweedfs-init jaeger"
|
||||
|
||||
. "$DEPLOY_DIR/scripts/lib.sh"
|
||||
|
||||
require_command docker
|
||||
require_command openssl
|
||||
|
||||
compose() {
|
||||
docker compose --env-file "$ENV_FILE" \
|
||||
-f "$DEPLOY_DIR/compose.yml" -f "$DEPLOY_DIR/compose.prod.yml" \
|
||||
--profile apps "$@"
|
||||
}
|
||||
|
||||
BUILD=--build
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-build) BUILD= ;;
|
||||
--down) step "Stopping the stack (volumes are kept)"; compose down --remove-orphans; exit 0 ;;
|
||||
*) fail "unknown option: $arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Preflight ------------------------------------------------------------------
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
cp "$DEPLOY_DIR/.env.prod.example" "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
fail "created $ENV_FILE from the template — fill it in (domain + secrets) and run again."
|
||||
fi
|
||||
|
||||
if grep -q 'CHANGE_ME\|example\.com' "$ENV_FILE"; then
|
||||
fail "$ENV_FILE still contains CHANGE_ME or example.com placeholders."
|
||||
fi
|
||||
|
||||
for cert in origin.pem origin.key; do
|
||||
[ -f "$DEPLOY_DIR/nginx/certs/$cert" ] \
|
||||
|| fail "missing nginx/certs/$cert (Cloudflare Origin Certificate)."
|
||||
done
|
||||
|
||||
mkdir -p "$DEPLOY_DIR/secrets" "$DEPLOY_DIR/keys" "$DEPLOY_DIR/zitadel/.output"
|
||||
case "$(env_get "$ENV_FILE" YTDLP_ADDITIONAL_ARGUMENTS)" in
|
||||
*--cookies*)
|
||||
[ -f "$DEPLOY_DIR/secrets/youtube_cookies.txt" ] \
|
||||
|| echo "warning: YTDLP_ADDITIONAL_ARGUMENTS expects secrets/youtube_cookies.txt, which is missing." ;;
|
||||
esac
|
||||
|
||||
step "Ensuring stream-ticket keys"
|
||||
sh "$DEPLOY_DIR/scripts/stream-keys.sh" "$DEPLOY_DIR/keys"
|
||||
|
||||
# Infrastructure -------------------------------------------------------------
|
||||
step "Starting the infrastructure"
|
||||
# shellcheck disable=SC2086
|
||||
compose up -d $INFRA
|
||||
|
||||
step "Waiting for Postgres"
|
||||
deadline=$(( $(date +%s) + 120 ))
|
||||
until [ "$(docker inspect --format '{{.State.Health.Status}}' musify-postgres 2>/dev/null)" = healthy ]; do
|
||||
[ "$(date +%s)" -lt "$deadline" ] || fail "Postgres did not become healthy in time."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# nginx has to be up before provisioning: it terminates TLS for the Zitadel
|
||||
# domain. --no-deps keeps the apps from starting before they have a client id.
|
||||
step "Starting the edge"
|
||||
compose up -d --no-deps nginx
|
||||
|
||||
# Zitadel + database ---------------------------------------------------------
|
||||
sh "$DEPLOY_DIR/zitadel/provision.sh"
|
||||
|
||||
step "Applying EF Core migrations"
|
||||
sh "$DEPLOY_DIR/scripts/migrate.sh"
|
||||
|
||||
# Applications ---------------------------------------------------------------
|
||||
step "Starting the applications"
|
||||
# shellcheck disable=SC2086
|
||||
compose up -d $BUILD
|
||||
|
||||
step "Done"
|
||||
compose ps
|
||||
cat <<EOF
|
||||
|
||||
Web player $(env_get "$ENV_FILE" PUBLIC_WEB_URL)
|
||||
API $(env_get "$ENV_FILE" PUBLIC_API_URL)/scalar/v1
|
||||
Streaming $(env_get "$ENV_FILE" PUBLIC_STREAM_URL)/health
|
||||
Zitadel $(env_get "$ENV_FILE" PUBLIC_AUTH_URL)/ui/console
|
||||
$(env_get "$ENV_FILE" ZITADEL_ADMIN_USERNAME)@zitadel.$(env_get "$ENV_FILE" ZITADEL_EXTERNAL_DOMAIN)
|
||||
|
||||
Jaeger collects traces but publishes no port. To look at them, forward its
|
||||
container address over SSH from your machine:
|
||||
ssh -L 16686:\$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' musify-jaeger):16686 <server>
|
||||
EOF
|
||||
@@ -1,157 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Provisions Zitadel for Musify in production (idempotent).
|
||||
|
||||
.DESCRIPTION
|
||||
Same idea as provision.ps1 but for the VPS deploy: creates the project, the
|
||||
API OIDC app (PKCE, JWT) and the Web OIDC app (confidential, JWT), then
|
||||
wires the values into deploy/.env.prod. Run from deploy/zitadel/ on the VPS
|
||||
after `docker compose ... up -d` has brought zitadel up.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ProjectName = "Musify",
|
||||
[string]$ApiAppName = "Musify API",
|
||||
[string]$WebAppName = "Musify Web"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$deployDir = Split-Path $PSScriptRoot -Parent
|
||||
$envPath = Join-Path $deployDir ".env.prod"
|
||||
$patFile = Join-Path $PSScriptRoot ".output/admin-sa.pat"
|
||||
|
||||
function Read-DotEnv($path) {
|
||||
$map = @{}
|
||||
Get-Content $path | ForEach-Object {
|
||||
if ($_ -match '^\s*#' -or $_ -notmatch '=') { return }
|
||||
$k, $v = $_.Split('=', 2)
|
||||
$map[$k.Trim()] = $v.Trim()
|
||||
}
|
||||
return $map
|
||||
}
|
||||
|
||||
$cfg = Read-DotEnv $envPath
|
||||
$issuer = "https://$($cfg['AUTH_DOMAIN'])"
|
||||
$apiUrl = "https://$($cfg['API_DOMAIN'])"
|
||||
$webUrl = "https://$($cfg['WEB_DOMAIN'])"
|
||||
|
||||
$deadline = (Get-Date).AddMinutes(3)
|
||||
while (-not (Test-Path $patFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 3 }
|
||||
if (-not (Test-Path $patFile)) { throw "PAT not found ($patFile). Check 'docker compose logs zitadel'." }
|
||||
|
||||
$headers = @{ Authorization = "Bearer $((Get-Content $patFile -Raw).Trim())" }
|
||||
$mgmt = "$issuer/management/v1"
|
||||
|
||||
function Invoke-Zitadel($method, $path, $body) {
|
||||
$params = @{ Method = $method; Uri = "$mgmt$path"; Headers = $headers; ContentType = "application/json"; TimeoutSec = 20 }
|
||||
if ($body) { $params.Body = ($body | ConvertTo-Json -Depth 10) }
|
||||
Invoke-RestMethod @params -SkipCertificateCheck
|
||||
}
|
||||
|
||||
Write-Host "Waiting for Zitadel at $issuer"
|
||||
$deadline = (Get-Date).AddMinutes(2)
|
||||
do {
|
||||
try { Invoke-RestMethod "$issuer/.well-known/openid-configuration" -TimeoutSec 5 -SkipCertificateCheck | Out-Null; $ready = $true }
|
||||
catch { $ready = $false; Start-Sleep 3 }
|
||||
} until ($ready -or (Get-Date) -gt $deadline)
|
||||
if (-not $ready) { throw "Zitadel did not respond in time." }
|
||||
|
||||
Write-Host "Disabling required Login UI v2 (use built-in /ui/login)"
|
||||
try {
|
||||
Invoke-RestMethod -Method PUT -Uri "$issuer/v2/features/instance" -Headers $headers `
|
||||
-ContentType "application/json" -Body (@{ loginV2 = @{ required = $false } } | ConvertTo-Json) -TimeoutSec 20 -SkipCertificateCheck | Out-Null
|
||||
} catch {
|
||||
Write-Host " warning: could not update loginV2 feature: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-Host "Project '$ProjectName'"
|
||||
$project = (Invoke-Zitadel POST "/projects/_search" @{
|
||||
queries = @(@{ nameQuery = @{ name = $ProjectName; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
||||
}).result | Where-Object { $_.name -eq $ProjectName } | Select-Object -First 1
|
||||
|
||||
if ($project) {
|
||||
$projectId = $project.id
|
||||
Write-Host " reused (id $projectId)"
|
||||
} else {
|
||||
$projectId = (Invoke-Zitadel POST "/projects" @{ name = $ProjectName }).id
|
||||
Write-Host " created (id $projectId)"
|
||||
}
|
||||
|
||||
Write-Host "OIDC app '$ApiAppName' (public, PKCE)"
|
||||
$apiApp = (Invoke-Zitadel POST "/projects/$projectId/apps/_search" @{
|
||||
queries = @(@{ nameQuery = @{ name = $ApiAppName; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
||||
}).result | Where-Object { $_.name -eq $ApiAppName } | Select-Object -First 1
|
||||
|
||||
if ($apiApp) {
|
||||
$apiClientId = (Invoke-Zitadel GET "/projects/$projectId/apps/$($apiApp.id)" $null).app.oidcConfig.clientId
|
||||
Write-Host " reused (clientId $apiClientId)"
|
||||
} else {
|
||||
$apiClientId = (Invoke-Zitadel POST "/projects/$projectId/apps/oidc" @{
|
||||
name = $ApiAppName
|
||||
redirectUris = @("$apiUrl/scalar/", "$apiUrl/scalar/oauth2-redirect.html")
|
||||
postLogoutRedirectUris = @("$apiUrl/scalar/")
|
||||
responseTypes = @("OIDC_RESPONSE_TYPE_CODE")
|
||||
grantTypes = @("OIDC_GRANT_TYPE_AUTHORIZATION_CODE")
|
||||
appType = "OIDC_APP_TYPE_USER_AGENT"
|
||||
authMethodType = "OIDC_AUTH_METHOD_TYPE_NONE"
|
||||
version = "OIDC_VERSION_1_0"
|
||||
devMode = $false
|
||||
accessTokenType = "OIDC_TOKEN_TYPE_JWT"
|
||||
accessTokenRoleAssertion = $true
|
||||
idTokenRoleAssertion = $true
|
||||
idTokenUserinfoAssertion = $true
|
||||
}).clientId
|
||||
Write-Host " created (clientId $apiClientId)"
|
||||
}
|
||||
|
||||
Write-Host "OIDC app '$WebAppName' (confidential)"
|
||||
$webApp = (Invoke-Zitadel POST "/projects/$projectId/apps/_search" @{
|
||||
queries = @(@{ nameQuery = @{ name = $WebAppName; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
||||
}).result | Where-Object { $_.name -eq $WebAppName } | Select-Object -First 1
|
||||
|
||||
$webClientSecret = $null
|
||||
if ($webApp) {
|
||||
$webClientId = (Invoke-Zitadel GET "/projects/$projectId/apps/$($webApp.id)" $null).app.oidcConfig.clientId
|
||||
Write-Host " reused (clientId $webClientId, secret NOT regenerated)"
|
||||
} else {
|
||||
$created = Invoke-Zitadel POST "/projects/$projectId/apps/oidc" @{
|
||||
name = $WebAppName
|
||||
redirectUris = @("$webUrl/auth/callback")
|
||||
postLogoutRedirectUris = @("$webUrl/")
|
||||
responseTypes = @("OIDC_RESPONSE_TYPE_CODE")
|
||||
grantTypes = @("OIDC_GRANT_TYPE_AUTHORIZATION_CODE")
|
||||
appType = "OIDC_APP_TYPE_WEB"
|
||||
authMethodType = "OIDC_AUTH_METHOD_TYPE_BASIC"
|
||||
version = "OIDC_VERSION_1_0"
|
||||
devMode = $false
|
||||
accessTokenType = "OIDC_TOKEN_TYPE_JWT"
|
||||
accessTokenRoleAssertion = $true
|
||||
idTokenRoleAssertion = $true
|
||||
idTokenUserinfoAssertion = $true
|
||||
}
|
||||
$webClientId = $created.clientId
|
||||
$webClientSecret = $created.clientSecret
|
||||
Write-Host " created (clientId $webClientId)"
|
||||
}
|
||||
|
||||
$envLines = Get-Content $envPath
|
||||
function Set-EnvValue([string[]]$lines, [string]$key, [string]$value) {
|
||||
if ($lines -match "^\s*$key=") {
|
||||
return $lines -replace "^\s*$key=.*", "$key=$value"
|
||||
}
|
||||
return $lines + "$key=$value"
|
||||
}
|
||||
$envLines = Set-EnvValue $envLines "AUTH_CLIENT_ID" $apiClientId
|
||||
$envLines = Set-EnvValue $envLines "WEB_CLIENT_ID" $webClientId
|
||||
if ($webClientSecret) {
|
||||
$envLines = Set-EnvValue $envLines "WEB_CLIENT_SECRET" $webClientSecret
|
||||
}
|
||||
Set-Content -Path $envPath -Value $envLines -Encoding utf8
|
||||
|
||||
Write-Host "`nZitadel provisioned:" -ForegroundColor Green
|
||||
Write-Host " Project: $ProjectName ($projectId)"
|
||||
Write-Host " API app: $apiClientId"
|
||||
Write-Host " Web app: $webClientId"
|
||||
Write-Host " Issuer: $issuer"
|
||||
if ($webClientSecret) { Write-Host " Web app secret written to .env.prod (WEB_CLIENT_SECRET)" }
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/bin/sh
|
||||
# Provisions Zitadel for Musify in production (idempotent).
|
||||
#
|
||||
# Creates the Musify project, the API OIDC app (public, PKCE, JWT access token)
|
||||
# and the web player OIDC app (confidential), then writes AUTH_CLIENT_ID,
|
||||
# WEB_CLIENT_ID and WEB_CLIENT_SECRET into deploy/.env.prod. Authenticates with
|
||||
# the service-account PAT that Zitadel writes on init. Re-running reuses the
|
||||
# existing project and apps.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
DEPLOY_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
ENV_FILE="$DEPLOY_DIR/.env.prod"
|
||||
PAT_FILE="$SCRIPT_DIR/.output/admin-sa.pat"
|
||||
|
||||
. "$DEPLOY_DIR/scripts/lib.sh"
|
||||
|
||||
require_command curl
|
||||
require_command jq
|
||||
|
||||
PROJECT_NAME=${PROJECT_NAME:-Musify}
|
||||
API_APP_NAME=${API_APP_NAME:-Musify API}
|
||||
WEB_APP_NAME=${WEB_APP_NAME:-Musify Web}
|
||||
|
||||
ISSUER=$(env_get "$ENV_FILE" PUBLIC_AUTH_URL)
|
||||
API_URL=$(env_get "$ENV_FILE" PUBLIC_API_URL)
|
||||
WEB_URL=$(env_get "$ENV_FILE" PUBLIC_WEB_URL)
|
||||
MGMT="$ISSUER/management/v1"
|
||||
|
||||
step "Provisioning Zitadel at $ISSUER"
|
||||
|
||||
# Zitadel writes the PAT during init; on a cold start it may take a moment.
|
||||
deadline=$(( $(date +%s) + 180 ))
|
||||
while [ ! -f "$PAT_FILE" ] && [ "$(date +%s)" -lt "$deadline" ]; do sleep 3; done
|
||||
[ -f "$PAT_FILE" ] || fail "PAT not found ($PAT_FILE). Check 'docker logs musify-zitadel'."
|
||||
PAT=$(tr -d '\r\n' <"$PAT_FILE")
|
||||
|
||||
api_call() {
|
||||
# api_call <method> <path> [json body]
|
||||
if [ $# -ge 3 ]; then
|
||||
curl -fsS -X "$1" "$MGMT$2" \
|
||||
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" -d "$3"
|
||||
else
|
||||
curl -fsS -X "$1" "$MGMT$2" -H "Authorization: Bearer $PAT"
|
||||
fi
|
||||
}
|
||||
|
||||
deadline=$(( $(date +%s) + 120 ))
|
||||
until curl -fsS "$ISSUER/.well-known/openid-configuration" >/dev/null 2>&1; do
|
||||
[ "$(date +%s)" -lt "$deadline" ] || fail "Zitadel did not respond at $ISSUER."
|
||||
sleep 3
|
||||
done
|
||||
|
||||
# The images tagged `latest` require the standalone Login UI v2 container, which
|
||||
# this stack does not run, so OIDC would redirect to /ui/v2/login and 404.
|
||||
curl -fsS -X PUT "$ISSUER/v2/features/instance" \
|
||||
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
|
||||
-d '{"loginV2":{"required":false}}' >/dev/null 2>&1 \
|
||||
|| echo " warning: could not disable the required Login UI v2"
|
||||
|
||||
name_query() {
|
||||
printf '{"queries":[{"nameQuery":{"name":"%s","method":"TEXT_QUERY_METHOD_EQUALS"}}]}' "$1"
|
||||
}
|
||||
|
||||
# Project --------------------------------------------------------------------
|
||||
PROJECT_ID=$(api_call POST "/projects/_search" "$(name_query "$PROJECT_NAME")" \
|
||||
| jq -r --arg n "$PROJECT_NAME" '.result // [] | map(select(.name == $n)) | first | .id // empty')
|
||||
|
||||
if [ -n "$PROJECT_ID" ]; then
|
||||
echo " project '$PROJECT_NAME' reused (id $PROJECT_ID)"
|
||||
else
|
||||
PROJECT_ID=$(api_call POST "/projects" "$(printf '{"name":"%s"}' "$PROJECT_NAME")" | jq -r '.id')
|
||||
echo " project '$PROJECT_NAME' created (id $PROJECT_ID)"
|
||||
fi
|
||||
|
||||
find_app() {
|
||||
api_call POST "/projects/$PROJECT_ID/apps/_search" "$(name_query "$1")" \
|
||||
| jq -r --arg n "$1" '.result // [] | map(select(.name == $n)) | first | .id // empty'
|
||||
}
|
||||
|
||||
# API app: public client used by the Scalar UI to obtain a token -------------
|
||||
API_APP_ID=$(find_app "$API_APP_NAME")
|
||||
if [ -n "$API_APP_ID" ]; then
|
||||
API_CLIENT_ID=$(api_call GET "/projects/$PROJECT_ID/apps/$API_APP_ID" | jq -r '.app.oidcConfig.clientId')
|
||||
echo " app '$API_APP_NAME' reused (clientId $API_CLIENT_ID)"
|
||||
else
|
||||
API_CLIENT_ID=$(api_call POST "/projects/$PROJECT_ID/apps/oidc" "$(jq -n \
|
||||
--arg name "$API_APP_NAME" --arg api "$API_URL" '{
|
||||
name: $name,
|
||||
redirectUris: [($api + "/scalar/"), ($api + "/scalar/oauth2-redirect.html")],
|
||||
postLogoutRedirectUris: [($api + "/scalar/")],
|
||||
responseTypes: ["OIDC_RESPONSE_TYPE_CODE"],
|
||||
grantTypes: ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
|
||||
appType: "OIDC_APP_TYPE_USER_AGENT",
|
||||
authMethodType: "OIDC_AUTH_METHOD_TYPE_NONE",
|
||||
version: "OIDC_VERSION_1_0",
|
||||
devMode: false,
|
||||
accessTokenType: "OIDC_TOKEN_TYPE_JWT",
|
||||
accessTokenRoleAssertion: true,
|
||||
idTokenRoleAssertion: true,
|
||||
idTokenUserinfoAssertion: true
|
||||
}')" | jq -r '.clientId')
|
||||
echo " app '$API_APP_NAME' created (clientId $API_CLIENT_ID)"
|
||||
fi
|
||||
|
||||
# Web app: confidential client used by the SvelteKit server ------------------
|
||||
WEB_APP_ID=$(find_app "$WEB_APP_NAME")
|
||||
WEB_CLIENT_SECRET=$(env_get "$ENV_FILE" WEB_CLIENT_SECRET)
|
||||
if [ -n "$WEB_APP_ID" ]; then
|
||||
WEB_CLIENT_ID=$(api_call GET "/projects/$PROJECT_ID/apps/$WEB_APP_ID" | jq -r '.app.oidcConfig.clientId')
|
||||
if [ -z "$WEB_CLIENT_SECRET" ]; then
|
||||
# The secret is only returned once, so regenerate it if we no longer have it.
|
||||
WEB_CLIENT_SECRET=$(api_call POST "/projects/$PROJECT_ID/apps/$WEB_APP_ID/oidc_config/_generate_client_secret" '{}' \
|
||||
| jq -r '.clientSecret')
|
||||
echo " app '$WEB_APP_NAME' reused (clientId $WEB_CLIENT_ID, secret regenerated)"
|
||||
else
|
||||
echo " app '$WEB_APP_NAME' reused (clientId $WEB_CLIENT_ID)"
|
||||
fi
|
||||
else
|
||||
CREATED=$(api_call POST "/projects/$PROJECT_ID/apps/oidc" "$(jq -n \
|
||||
--arg name "$WEB_APP_NAME" --arg web "$WEB_URL" '{
|
||||
name: $name,
|
||||
redirectUris: [($web + "/auth/callback")],
|
||||
postLogoutRedirectUris: [($web + "/")],
|
||||
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",
|
||||
version: "OIDC_VERSION_1_0",
|
||||
devMode: false,
|
||||
accessTokenType: "OIDC_TOKEN_TYPE_JWT",
|
||||
accessTokenRoleAssertion: true,
|
||||
idTokenRoleAssertion: true,
|
||||
idTokenUserinfoAssertion: true
|
||||
}')")
|
||||
WEB_CLIENT_ID=$(echo "$CREATED" | jq -r '.clientId')
|
||||
WEB_CLIENT_SECRET=$(echo "$CREATED" | jq -r '.clientSecret')
|
||||
echo " app '$WEB_APP_NAME' created (clientId $WEB_CLIENT_ID)"
|
||||
fi
|
||||
|
||||
env_set "$ENV_FILE" AUTH_CLIENT_ID "$API_CLIENT_ID"
|
||||
env_set "$ENV_FILE" WEB_CLIENT_ID "$WEB_CLIENT_ID"
|
||||
env_set "$ENV_FILE" WEB_CLIENT_SECRET "$WEB_CLIENT_SECRET"
|
||||
|
||||
echo " wired into $(basename "$ENV_FILE")"
|
||||
Reference in New Issue
Block a user