feat(deploy): provision the web player client in development too
The web player's OIDC app had to be created by hand in the Zitadel console and its client id copied into web-player/.env. provision.ps1 now creates both apps, regenerates the client secret when it is no longer known, and wires the values into deploy/.env, Musify.Api user-secrets and web-player/.env. up.ps1 follows the new compose files, gains -Apps and -Tools for the two profiles, generates the keys before starting rather than after, and no longer swallows a provisioning failure as a warning.
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
#requires -Version 5.1
|
#requires -Version 5.1
|
||||||
# Shared helpers for the deploy scripts.
|
# Shared helpers for the development deploy scripts.
|
||||||
|
|
||||||
function Write-Step([string]$Message) { Write-Host "`n==> $Message" -ForegroundColor Cyan }
|
function Write-Step([string]$Message) { Write-Host "`n==> $Message" -ForegroundColor Cyan }
|
||||||
|
|
||||||
# Parses a .env file into a hashtable (ignores comments and blank lines).
|
# Parses a .env file into an ordered hashtable (ignores comments and blank lines).
|
||||||
function Read-DotEnv([string]$Path) {
|
function Read-DotEnv([string]$Path) {
|
||||||
$values = @{}
|
$values = [ordered]@{}
|
||||||
if (Test-Path $Path) {
|
if (Test-Path $Path) {
|
||||||
Get-Content $Path | ForEach-Object {
|
Get-Content $Path | ForEach-Object {
|
||||||
if ($_ -match '^\s*([^#=\s][^=]*)=(.*)$') { $values[$matches[1].Trim()] = $matches[2].Trim() }
|
if ($_ -match '^\s*([^#=\s][^=]*)=(.*)$') { $values[$matches[1].Trim()] = $matches[2].Trim() }
|
||||||
@@ -13,3 +13,14 @@ function Read-DotEnv([string]$Path) {
|
|||||||
}
|
}
|
||||||
return $values
|
return $values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Sets (or appends) a key in a .env file, preserving the rest of the file.
|
||||||
|
function Set-DotEnvValue([string]$Path, [string]$Key, [string]$Value) {
|
||||||
|
$lines = @(Get-Content $Path)
|
||||||
|
if ($lines -match "^\s*$Key=") {
|
||||||
|
$lines = $lines -replace "^\s*$Key=.*", "$Key=$Value"
|
||||||
|
} else {
|
||||||
|
$lines += "$Key=$Value"
|
||||||
|
}
|
||||||
|
Set-Content -Path $Path -Value $lines -Encoding utf8
|
||||||
|
}
|
||||||
|
|||||||
+62
-39
@@ -4,25 +4,35 @@
|
|||||||
Brings up the Musify development environment.
|
Brings up the Musify development environment.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Starts the infrastructure stack, generates the stream-ticket keys, applies EF
|
Starts the infrastructure stack, generates the stream-ticket keys, applies
|
||||||
Core migrations and provisions Zitadel (project + OIDC app + ClientId).
|
the EF Core migrations and provisions Zitadel (project + OIDC apps). The
|
||||||
|
.NET apps and the web player then run from the IDE; add -Apps to run them
|
||||||
|
in Docker instead.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
./up.ps1 # Bring everything up
|
./up.ps1 # Infrastructure only (apps from the IDE)
|
||||||
|
./up.ps1 -Apps # Also build and run api, worker, gateway, web player
|
||||||
|
./up.ps1 -Tools # Also start pgAdmin
|
||||||
./up.ps1 -SkipMigrations # Skip the database migration step
|
./up.ps1 -SkipMigrations # Skip the database migration step
|
||||||
./up.ps1 -Down # Stop and remove containers (keep volumes)
|
./up.ps1 -Down # Stop and remove containers (keep volumes)
|
||||||
./up.ps1 -Destroy # Stop and remove containers + volumes (wipe data)
|
./up.ps1 -Destroy # Stop and remove containers + volumes (wipe data)
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
|
[switch]$Apps,
|
||||||
|
[switch]$Tools,
|
||||||
|
[switch]$SkipMigrations,
|
||||||
[switch]$Down,
|
[switch]$Down,
|
||||||
[switch]$Destroy,
|
[switch]$Destroy
|
||||||
[switch]$SkipMigrations
|
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$deployDir = $PSScriptRoot
|
$deployDir = $PSScriptRoot
|
||||||
$composeFile = Join-Path $deployDir "docker-compose.yml"
|
$compose = @(
|
||||||
|
"-f", (Join-Path $deployDir "compose.yml"),
|
||||||
|
"-f", (Join-Path $deployDir "compose.dev.yml")
|
||||||
|
)
|
||||||
|
$profiles = @("--profile", "apps", "--profile", "tools")
|
||||||
|
|
||||||
. (Join-Path $deployDir "scripts\common.ps1")
|
. (Join-Path $deployDir "scripts\common.ps1")
|
||||||
|
|
||||||
@@ -30,12 +40,12 @@ $composeFile = Join-Path $deployDir "docker-compose.yml"
|
|||||||
if ($Down -or $Destroy) {
|
if ($Down -or $Destroy) {
|
||||||
if ($Destroy) {
|
if ($Destroy) {
|
||||||
Write-Step "Stopping the stack and REMOVING volumes (data is wiped)"
|
Write-Step "Stopping the stack and REMOVING volumes (data is wiped)"
|
||||||
docker compose -f $composeFile down -v --remove-orphans
|
docker compose @compose @profiles down -v --remove-orphans
|
||||||
# Drop the Zitadel init secrets (PAT) so the next start re-initializes cleanly.
|
# Drop the Zitadel init secrets so the next start re-initializes cleanly.
|
||||||
Get-ChildItem (Join-Path $deployDir "zitadel\.output") -File -ErrorAction SilentlyContinue | Remove-Item -Force
|
Get-ChildItem (Join-Path $deployDir "zitadel\.output") -File -ErrorAction SilentlyContinue | Remove-Item -Force
|
||||||
} else {
|
} else {
|
||||||
Write-Step "Stopping the stack (volumes are kept)"
|
Write-Step "Stopping the stack (volumes are kept)"
|
||||||
docker compose -f $composeFile down --remove-orphans
|
docker compose @compose @profiles down --remove-orphans
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -47,12 +57,18 @@ if (-not (Test-Path $envFile)) {
|
|||||||
Copy-Item (Join-Path $deployDir ".env.example") $envFile
|
Copy-Item (Join-Path $deployDir ".env.example") $envFile
|
||||||
}
|
}
|
||||||
$cfg = Read-DotEnv $envFile
|
$cfg = Read-DotEnv $envFile
|
||||||
$streamKeysDir = if ($cfg['STREAM_KEYS_DIR']) { $cfg['STREAM_KEYS_DIR'] } else { "D:/weed" }
|
|
||||||
$workerTempDir = if ($cfg['WORKER_TEMP_DIR']) { $cfg['WORKER_TEMP_DIR'] } else { "D:/tempsFilesDev" }
|
# Host directories the apps expect: RS256 keys (mounted into the api/gateway
|
||||||
|
# containers, read directly by the IDE runs) and the Worker scratch space.
|
||||||
|
& (Join-Path $deployDir "scripts\stream-keys.ps1") -KeysDir (Join-Path $deployDir "keys")
|
||||||
|
$tempDir = Join-Path $deployDir ".tmp"
|
||||||
|
if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir | Out-Null }
|
||||||
|
|
||||||
# Infrastructure -------------------------------------------------------------
|
# Infrastructure -------------------------------------------------------------
|
||||||
Write-Step "Starting the infrastructure stack"
|
Write-Step "Starting the infrastructure stack"
|
||||||
docker compose -f $composeFile up -d
|
$upArgs = @("up", "-d")
|
||||||
|
if ($Tools) { $upArgs = @("--profile", "tools") + $upArgs }
|
||||||
|
docker compose @compose @upArgs
|
||||||
if ($LASTEXITCODE -ne 0) { throw "docker compose up failed" }
|
if ($LASTEXITCODE -ne 0) { throw "docker compose up failed" }
|
||||||
|
|
||||||
Write-Step "Waiting for Postgres to become healthy"
|
Write-Step "Waiting for Postgres to become healthy"
|
||||||
@@ -63,16 +79,6 @@ do {
|
|||||||
} until ($status -eq "healthy" -or (Get-Date) -gt $deadline)
|
} until ($status -eq "healthy" -or (Get-Date) -gt $deadline)
|
||||||
if ($status -ne "healthy") { throw "Postgres did not become healthy in time" }
|
if ($status -ne "healthy") { throw "Postgres did not become healthy in time" }
|
||||||
|
|
||||||
# Stream-ticket keys ---------------------------------------------------------
|
|
||||||
Write-Step "Ensuring stream-ticket keys"
|
|
||||||
& (Join-Path $deployDir "scripts\stream-keys.ps1") -KeysDir $streamKeysDir
|
|
||||||
|
|
||||||
# Worker temp directory ------------------------------------------------------
|
|
||||||
if (-not (Test-Path $workerTempDir)) {
|
|
||||||
Write-Step "Creating the Worker temp directory: $workerTempDir"
|
|
||||||
New-Item -ItemType Directory -Path $workerTempDir | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Database migrations --------------------------------------------------------
|
# Database migrations --------------------------------------------------------
|
||||||
if ($SkipMigrations) {
|
if ($SkipMigrations) {
|
||||||
Write-Host "`nSkipping migrations (-SkipMigrations)."
|
Write-Host "`nSkipping migrations (-SkipMigrations)."
|
||||||
@@ -83,16 +89,36 @@ if ($SkipMigrations) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Zitadel provisioning -------------------------------------------------------
|
# Zitadel provisioning -------------------------------------------------------
|
||||||
Write-Step "Provisioning Zitadel (project + OIDC app + ClientId)"
|
# Writes the client ids into .env, Musify.Api user-secrets and web-player/.env,
|
||||||
try {
|
# so it has to run before the app containers start.
|
||||||
& (Join-Path $deployDir "zitadel\provision.ps1")
|
& (Join-Path $deployDir "zitadel\provision.ps1")
|
||||||
} catch {
|
|
||||||
Write-Host "Warning: Zitadel provisioning failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
# Applications ---------------------------------------------------------------
|
||||||
|
if ($Apps) {
|
||||||
|
Write-Step "Building and starting the applications"
|
||||||
|
docker compose @compose --profile apps up -d --build
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "docker compose up (apps) failed" }
|
||||||
}
|
}
|
||||||
|
|
||||||
# Summary --------------------------------------------------------------------
|
# Summary --------------------------------------------------------------------
|
||||||
$zDomain = $cfg['ZITADEL_EXTERNAL_DOMAIN']
|
$cfg = Read-DotEnv $envFile
|
||||||
$zPort = $cfg['ZITADEL_EXTERNAL_PORT']
|
$authUrl = $cfg['PUBLIC_AUTH_URL']
|
||||||
|
$appLines = if ($Apps) {
|
||||||
|
@"
|
||||||
|
Musify API $($cfg['PUBLIC_API_URL'])/scalar/v1
|
||||||
|
Streaming gateway $($cfg['PUBLIC_STREAM_URL'])/health
|
||||||
|
Web player $($cfg['PUBLIC_WEB_URL'])
|
||||||
|
"@
|
||||||
|
} else {
|
||||||
|
@"
|
||||||
|
Run the apps from the IDE (or ./up.ps1 -Apps to run them in Docker):
|
||||||
|
dotnet run --project backend/Musify.Api # :5111
|
||||||
|
dotnet run --project backend/Musify.StreamingGateway # :8081
|
||||||
|
dotnet run --project backend/Musify.Worker # background
|
||||||
|
npm --prefix web-player run dev # :5173
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host @"
|
Write-Host @"
|
||||||
|
|
||||||
============================================================
|
============================================================
|
||||||
@@ -102,21 +128,18 @@ Write-Host @"
|
|||||||
Service URL / port
|
Service URL / port
|
||||||
------------------ --------------------------------------
|
------------------ --------------------------------------
|
||||||
PostgreSQL localhost:$($cfg['POSTGRES_HOST_PORT']) (db: $($cfg['POSTGRES_DB']))
|
PostgreSQL localhost:$($cfg['POSTGRES_HOST_PORT']) (db: $($cfg['POSTGRES_DB']))
|
||||||
pgAdmin http://localhost:$($cfg['PGADMIN_HOST_PORT']) ($($cfg['PGADMIN_DEFAULT_EMAIL'])/$($cfg['PGADMIN_DEFAULT_PASSWORD']))
|
Zitadel console $authUrl/ui/console
|
||||||
Zitadel console http://$zDomain`:$zPort/ui/console
|
|
||||||
RabbitMQ mgmt http://localhost:15672 ($($cfg['RABBITMQ_DEFAULT_USER'])/$($cfg['RABBITMQ_DEFAULT_PASS']))
|
RabbitMQ mgmt http://localhost:15672 ($($cfg['RABBITMQ_DEFAULT_USER'])/$($cfg['RABBITMQ_DEFAULT_PASS']))
|
||||||
SeaweedFS S3 http://localhost:8333 (bucket: $($cfg['S3_BUCKET']))
|
SeaweedFS S3 http://localhost:8333 (bucket: $($cfg['S3_BUCKET']))
|
||||||
SeaweedFS filer http://localhost:8888
|
SeaweedFS filer http://localhost:8888
|
||||||
SeaweedFS master http://localhost:9333
|
SeaweedFS master http://localhost:9333
|
||||||
Jaeger traces http://localhost:16686 (OTLP: localhost:4317)
|
Jaeger traces http://localhost:16686 (OTLP: localhost:4317)
|
||||||
|
pgAdmin (-Tools) http://localhost:$($cfg['PGADMIN_HOST_PORT']) ($($cfg['PGADMIN_DEFAULT_EMAIL'])/$($cfg['PGADMIN_DEFAULT_PASSWORD']))
|
||||||
|
|
||||||
Zitadel admin: $($cfg['ZITADEL_ADMIN_USERNAME'])@zitadel.$zDomain / $($cfg['ZITADEL_ADMIN_PASSWORD'])
|
Zitadel admin $($cfg['ZITADEL_ADMIN_USERNAME'])@zitadel.$($cfg['ZITADEL_EXTERNAL_DOMAIN']) / $($cfg['ZITADEL_ADMIN_PASSWORD'])
|
||||||
Auth is provisioned automatically; ClientId is in Musify.Api user-secrets and .env.
|
API client id $($cfg['AUTH_CLIENT_ID'])
|
||||||
Log in via Scalar: http://localhost:5111/scalar/v1
|
Web client id $($cfg['WEB_CLIENT_ID'])
|
||||||
|
|
||||||
Run the apps from the IDE or:
|
$appLines
|
||||||
dotnet run --project backend/Musify.Api # :5111
|
|
||||||
dotnet run --project backend/Musify.StreamingGateway # :8081
|
|
||||||
dotnet run --project backend/Musify.Worker # background
|
|
||||||
============================================================
|
============================================================
|
||||||
"@ -ForegroundColor Green
|
"@ -ForegroundColor Green
|
||||||
|
|||||||
+108
-60
@@ -1,31 +1,40 @@
|
|||||||
#requires -Version 5.1
|
#requires -Version 5.1
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Provisions Zitadel for Musify (idempotent).
|
Provisions Zitadel for Musify in development (idempotent).
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Creates the project and the OIDC app (PKCE, JWT access token) and wires the
|
Creates the Musify project, the API OIDC app (public, PKCE, JWT access
|
||||||
generated ClientId into Musify.Api user-secrets and deploy/.env (AUTH_CLIENT_ID).
|
token) and the web player OIDC app (confidential), then wires the resulting
|
||||||
Uses the service-account PAT that Zitadel writes on init (zitadel/.output/admin-sa.pat).
|
client ids into deploy/.env, Musify.Api user-secrets and web-player/.env.
|
||||||
Re-running reuses the existing project/app and refreshes the wired values.
|
Authenticates with the service-account PAT that Zitadel writes on init
|
||||||
|
(zitadel/.output/admin-sa.pat). Re-running reuses the existing project and
|
||||||
|
apps and just refreshes the wired values.
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ProjectName = "Musify",
|
[string]$ProjectName = "Musify",
|
||||||
[string]$AppName = "Musify API (Scalar)"
|
[string]$ApiAppName = "Musify API (Scalar)",
|
||||||
|
[string]$WebAppName = "Musify Web"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$deployDir = Split-Path $PSScriptRoot -Parent
|
$deployDir = Split-Path $PSScriptRoot -Parent
|
||||||
$apiProject = Resolve-Path (Join-Path $deployDir "..\backend\Musify.Api")
|
$repoDir = Split-Path $deployDir -Parent
|
||||||
|
$apiProject = Join-Path $repoDir "backend\Musify.Api"
|
||||||
|
$webEnvFile = Join-Path $repoDir "web-player\.env"
|
||||||
|
$envFile = Join-Path $deployDir ".env"
|
||||||
$patFile = Join-Path $PSScriptRoot ".output\admin-sa.pat"
|
$patFile = Join-Path $PSScriptRoot ".output\admin-sa.pat"
|
||||||
|
|
||||||
. (Join-Path $deployDir "scripts\common.ps1")
|
. (Join-Path $deployDir "scripts\common.ps1")
|
||||||
|
|
||||||
$cfg = Read-DotEnv (Join-Path $deployDir ".env")
|
$cfg = Read-DotEnv $envFile
|
||||||
$issuer = "http://$($cfg['ZITADEL_EXTERNAL_DOMAIN']):$($cfg['ZITADEL_EXTERNAL_PORT'])"
|
$issuer = $cfg['PUBLIC_AUTH_URL']
|
||||||
$apiBase = "http://localhost:5111"
|
$apiUrl = $cfg['PUBLIC_API_URL']
|
||||||
$scalarRedirect = "$apiBase/scalar/"
|
$webUrl = $cfg['PUBLIC_WEB_URL']
|
||||||
|
$viteUrl = "http://localhost:5173" # `npm run dev` in web-player/
|
||||||
|
|
||||||
|
Write-Step "Provisioning Zitadel at $issuer"
|
||||||
|
|
||||||
# Zitadel writes the PAT during init; on a cold start it may take a moment.
|
# Zitadel writes the PAT during init; on a cold start it may take a moment.
|
||||||
$deadline = (Get-Date).AddMinutes(3)
|
$deadline = (Get-Date).AddMinutes(3)
|
||||||
@@ -41,7 +50,6 @@ function Invoke-Zitadel($method, $path, $body) {
|
|||||||
Invoke-RestMethod @params
|
Invoke-RestMethod @params
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Step "Waiting for Zitadel at $issuer"
|
|
||||||
$deadline = (Get-Date).AddMinutes(2)
|
$deadline = (Get-Date).AddMinutes(2)
|
||||||
do {
|
do {
|
||||||
try { Invoke-RestMethod "$issuer/.well-known/openid-configuration" -TimeoutSec 5 | Out-Null; $ready = $true }
|
try { Invoke-RestMethod "$issuer/.well-known/openid-configuration" -TimeoutSec 5 | Out-Null; $ready = $true }
|
||||||
@@ -49,86 +57,126 @@ do {
|
|||||||
} until ($ready -or (Get-Date) -gt $deadline)
|
} until ($ready -or (Get-Date) -gt $deadline)
|
||||||
if (-not $ready) { throw "Zitadel did not respond in time." }
|
if (-not $ready) { throw "Zitadel did not respond in time." }
|
||||||
|
|
||||||
# Login UI (idempotent) ------------------------------------------------------
|
# The images tagged `latest` require the standalone Login UI v2 container, which
|
||||||
# The 'latest' Zitadel image requires the Login UI v2 (a separate 'login'
|
# this stack does not run, so OIDC would redirect to /ui/v2/login and 404.
|
||||||
# container that this stack does not run), so OIDC would redirect to
|
# Opting out keeps the built-in v1 login.
|
||||||
# /ui/v2/login and 404. Disable the requirement to use the built-in v1 login.
|
|
||||||
Write-Step "Disabling required Login UI v2 (use built-in /ui/login)"
|
|
||||||
try {
|
try {
|
||||||
Invoke-RestMethod -Method PUT -Uri "$issuer/v2/features/instance" -Headers $headers `
|
Invoke-RestMethod -Method PUT -Uri "$issuer/v2/features/instance" -Headers $headers `
|
||||||
-ContentType "application/json" -Body (@{ loginV2 = @{ required = $false } } | ConvertTo-Json) -TimeoutSec 20 | Out-Null
|
-ContentType "application/json" -Body (@{ loginV2 = @{ required = $false } } | ConvertTo-Json) -TimeoutSec 20 | Out-Null
|
||||||
Write-Host " loginV2.required = false"
|
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host " warning: could not update loginV2 feature: $($_.Exception.Message)"
|
Write-Host " warning: could not disable the required Login UI v2: $($_.Exception.Message)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Project (idempotent) -------------------------------------------------------
|
# Project --------------------------------------------------------------------
|
||||||
Write-Step "Project '$ProjectName'"
|
|
||||||
$project = (Invoke-Zitadel POST "/projects/_search" @{
|
$project = (Invoke-Zitadel POST "/projects/_search" @{
|
||||||
queries = @(@{ nameQuery = @{ name = $ProjectName; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
queries = @(@{ nameQuery = @{ name = $ProjectName; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
||||||
}).result | Where-Object { $_.name -eq $ProjectName } | Select-Object -First 1
|
}).result | Where-Object { $_.name -eq $ProjectName } | Select-Object -First 1
|
||||||
|
|
||||||
if ($project) {
|
if ($project) {
|
||||||
$projectId = $project.id
|
$projectId = $project.id
|
||||||
Write-Host " reused (id $projectId)"
|
Write-Host " project '$ProjectName' reused (id $projectId)"
|
||||||
} else {
|
} else {
|
||||||
$projectId = (Invoke-Zitadel POST "/projects" @{ name = $ProjectName }).id
|
$projectId = (Invoke-Zitadel POST "/projects" @{ name = $ProjectName }).id
|
||||||
Write-Host " created (id $projectId)"
|
Write-Host " project '$ProjectName' created (id $projectId)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# OIDC app (idempotent) ------------------------------------------------------
|
function Get-ZitadelApp($name) {
|
||||||
Write-Step "OIDC app '$AppName'"
|
(Invoke-Zitadel POST "/projects/$projectId/apps/_search" @{
|
||||||
$app = (Invoke-Zitadel POST "/projects/$projectId/apps/_search" @{
|
queries = @(@{ nameQuery = @{ name = $name; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
||||||
queries = @(@{ nameQuery = @{ name = $AppName; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
}).result | Where-Object { $_.name -eq $name } | Select-Object -First 1
|
||||||
}).result | Where-Object { $_.name -eq $AppName } | Select-Object -First 1
|
}
|
||||||
|
|
||||||
if ($app) {
|
# API app: public client used by the Scalar UI to obtain a token -------------
|
||||||
$clientId = (Invoke-Zitadel GET "/projects/$projectId/apps/$($app.id)" $null).app.oidcConfig.clientId
|
$apiApp = Get-ZitadelApp $ApiAppName
|
||||||
Write-Host " reused (clientId $clientId)"
|
if ($apiApp) {
|
||||||
|
$apiClientId = (Invoke-Zitadel GET "/projects/$projectId/apps/$($apiApp.id)" $null).app.oidcConfig.clientId
|
||||||
|
Write-Host " app '$ApiAppName' reused (clientId $apiClientId)"
|
||||||
} else {
|
} else {
|
||||||
$clientId = (Invoke-Zitadel POST "/projects/$projectId/apps/oidc" @{
|
$apiClientId = (Invoke-Zitadel POST "/projects/$projectId/apps/oidc" @{
|
||||||
name = $AppName
|
name = $ApiAppName
|
||||||
redirectUris = @($scalarRedirect, "$apiBase/scalar/oauth2-redirect.html")
|
redirectUris = @("$apiUrl/scalar/", "$apiUrl/scalar/oauth2-redirect.html")
|
||||||
postLogoutRedirectUris = @($scalarRedirect)
|
postLogoutRedirectUris = @("$apiUrl/scalar/")
|
||||||
responseTypes = @("OIDC_RESPONSE_TYPE_CODE")
|
responseTypes = @("OIDC_RESPONSE_TYPE_CODE")
|
||||||
grantTypes = @("OIDC_GRANT_TYPE_AUTHORIZATION_CODE")
|
grantTypes = @("OIDC_GRANT_TYPE_AUTHORIZATION_CODE")
|
||||||
appType = "OIDC_APP_TYPE_USER_AGENT"
|
appType = "OIDC_APP_TYPE_USER_AGENT"
|
||||||
authMethodType = "OIDC_AUTH_METHOD_TYPE_NONE"
|
authMethodType = "OIDC_AUTH_METHOD_TYPE_NONE"
|
||||||
version = "OIDC_VERSION_1_0"
|
version = "OIDC_VERSION_1_0"
|
||||||
devMode = $true # allow http redirect URIs
|
devMode = $true # allow http redirect URIs
|
||||||
accessTokenType = "OIDC_TOKEN_TYPE_JWT" # required so JwtBearer can validate
|
accessTokenType = "OIDC_TOKEN_TYPE_JWT" # required so JwtBearer can validate it
|
||||||
accessTokenRoleAssertion = $true
|
accessTokenRoleAssertion = $true
|
||||||
idTokenRoleAssertion = $true
|
idTokenRoleAssertion = $true
|
||||||
idTokenUserinfoAssertion = $true
|
idTokenUserinfoAssertion = $true
|
||||||
}).clientId
|
}).clientId
|
||||||
Write-Host " created (clientId $clientId)"
|
Write-Host " app '$ApiAppName' created (clientId $apiClientId)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Wire the config: user-secrets (local runs) + .env (docker runs) -------------
|
# Web app: confidential client used by the SvelteKit server ------------------
|
||||||
Write-Step "Wiring Authentication into Musify.Api user-secrets"
|
$webApp = Get-ZitadelApp $WebAppName
|
||||||
|
$webSecret = $cfg['WEB_CLIENT_SECRET']
|
||||||
|
if ($webApp) {
|
||||||
|
$webClientId = (Invoke-Zitadel GET "/projects/$projectId/apps/$($webApp.id)" $null).app.oidcConfig.clientId
|
||||||
|
# The secret is only returned once, so regenerate it if we no longer have it.
|
||||||
|
if (-not $webSecret) {
|
||||||
|
$webSecret = (Invoke-Zitadel POST "/projects/$projectId/apps/$($webApp.id)/oidc_config/_generate_client_secret" @{}).clientSecret
|
||||||
|
Write-Host " app '$WebAppName' reused (clientId $webClientId, secret regenerated)"
|
||||||
|
} else {
|
||||||
|
Write-Host " app '$WebAppName' reused (clientId $webClientId)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$created = Invoke-Zitadel POST "/projects/$projectId/apps/oidc" @{
|
||||||
|
name = $WebAppName
|
||||||
|
redirectUris = @("$webUrl/auth/callback", "$viteUrl/auth/callback")
|
||||||
|
postLogoutRedirectUris = @("$webUrl/", "$viteUrl/")
|
||||||
|
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 = $true
|
||||||
|
accessTokenType = "OIDC_TOKEN_TYPE_JWT"
|
||||||
|
accessTokenRoleAssertion = $true
|
||||||
|
idTokenRoleAssertion = $true
|
||||||
|
idTokenUserinfoAssertion = $true
|
||||||
|
}
|
||||||
|
$webClientId = $created.clientId
|
||||||
|
$webSecret = $created.clientSecret
|
||||||
|
Write-Host " app '$WebAppName' created (clientId $webClientId)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wire the values ------------------------------------------------------------
|
||||||
|
Set-DotEnvValue $envFile "AUTH_CLIENT_ID" $apiClientId
|
||||||
|
Set-DotEnvValue $envFile "WEB_CLIENT_ID" $webClientId
|
||||||
|
Set-DotEnvValue $envFile "WEB_CLIENT_SECRET" $webSecret
|
||||||
|
|
||||||
|
# Musify.Api reads these from user-secrets when it runs outside Docker.
|
||||||
@{
|
@{
|
||||||
"Authentication:ClientId" = $clientId
|
"Authentication:ClientId" = $apiClientId
|
||||||
"Authentication:AudienceAddress" = $clientId
|
"Authentication:AudienceAddress" = $apiClientId
|
||||||
"Authentication:IssuerAddress" = $issuer
|
"Authentication:IssuerAddress" = $issuer
|
||||||
"Authentication:MetadataAddress" = "$issuer/.well-known/openid-configuration"
|
"Authentication:MetadataAddress" = "$issuer/.well-known/openid-configuration"
|
||||||
"Authentication:AuthorizationEndpoint" = "$issuer/oauth/v2/authorize?prompt=login"
|
"Authentication:AuthorizationEndpoint" = "$issuer/oauth/v2/authorize?prompt=login"
|
||||||
"Authentication:TokenEndpoint" = "$issuer/oauth/v2/token"
|
"Authentication:TokenEndpoint" = "$issuer/oauth/v2/token"
|
||||||
"Authentication:ScalarRedirectUri" = $scalarRedirect
|
"Authentication:ScalarRedirectUri" = "$apiUrl/scalar/"
|
||||||
"Authentication:RequireHttpsMetadata" = "false"
|
"Authentication:RequireHttpsMetadata" = "false"
|
||||||
}.GetEnumerator() | ForEach-Object {
|
}.GetEnumerator() | ForEach-Object {
|
||||||
dotnet user-secrets set $_.Key $_.Value --project $apiProject | Out-Null
|
dotnet user-secrets set $_.Key $_.Value --project $apiProject | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
$envPath = Join-Path $deployDir ".env"
|
# web-player/.env is what `npm run dev` reads (the container gets its config
|
||||||
$envLines = Get-Content $envPath
|
# from compose instead).
|
||||||
if ($envLines -match '^\s*AUTH_CLIENT_ID=') {
|
if (-not (Test-Path $webEnvFile)) {
|
||||||
$envLines = $envLines -replace '^\s*AUTH_CLIENT_ID=.*', "AUTH_CLIENT_ID=$clientId"
|
Copy-Item (Join-Path $repoDir "web-player\.env.example") $webEnvFile
|
||||||
} else {
|
}
|
||||||
$envLines += "AUTH_CLIENT_ID=$clientId"
|
@{
|
||||||
|
ZITADEL_ISSUER = $issuer
|
||||||
|
ZITADEL_CLIENT_ID = $webClientId
|
||||||
|
ZITADEL_CLIENT_SECRET = $webSecret
|
||||||
|
AUTH_REDIRECT_URI = "$viteUrl/auth/callback"
|
||||||
|
AUTH_POST_LOGOUT_URI = "$viteUrl/"
|
||||||
|
SESSION_SECRET = $cfg['SESSION_SECRET']
|
||||||
|
API_BASE_URL = $apiUrl
|
||||||
|
}.GetEnumerator() | ForEach-Object {
|
||||||
|
Set-DotEnvValue $webEnvFile $_.Key $_.Value
|
||||||
}
|
}
|
||||||
Set-Content -Path $envPath -Value $envLines -Encoding utf8
|
|
||||||
|
|
||||||
Write-Host "`nZitadel provisioned:" -ForegroundColor Green
|
Write-Host " wired into deploy/.env, Musify.Api user-secrets and web-player/.env"
|
||||||
Write-Host " Project: $ProjectName ($projectId)"
|
|
||||||
Write-Host " ClientId: $clientId"
|
|
||||||
Write-Host " Issuer: $issuer"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user