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:
+108
-60
@@ -1,31 +1,40 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Provisions Zitadel for Musify (idempotent).
|
||||
Provisions Zitadel for Musify in development (idempotent).
|
||||
|
||||
.DESCRIPTION
|
||||
Creates the project and the OIDC app (PKCE, JWT access token) and wires the
|
||||
generated ClientId into Musify.Api user-secrets and deploy/.env (AUTH_CLIENT_ID).
|
||||
Uses the service-account PAT that Zitadel writes on init (zitadel/.output/admin-sa.pat).
|
||||
Re-running reuses the existing project/app and refreshes the wired values.
|
||||
Creates the Musify project, the API OIDC app (public, PKCE, JWT access
|
||||
token) and the web player OIDC app (confidential), then wires the resulting
|
||||
client ids into deploy/.env, Musify.Api user-secrets and web-player/.env.
|
||||
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()]
|
||||
param(
|
||||
[string]$ProjectName = "Musify",
|
||||
[string]$AppName = "Musify API (Scalar)"
|
||||
[string]$ApiAppName = "Musify API (Scalar)",
|
||||
[string]$WebAppName = "Musify Web"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$deployDir = Split-Path $PSScriptRoot -Parent
|
||||
$apiProject = Resolve-Path (Join-Path $deployDir "..\backend\Musify.Api")
|
||||
$deployDir = Split-Path $PSScriptRoot -Parent
|
||||
$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"
|
||||
|
||||
. (Join-Path $deployDir "scripts\common.ps1")
|
||||
|
||||
$cfg = Read-DotEnv (Join-Path $deployDir ".env")
|
||||
$issuer = "http://$($cfg['ZITADEL_EXTERNAL_DOMAIN']):$($cfg['ZITADEL_EXTERNAL_PORT'])"
|
||||
$apiBase = "http://localhost:5111"
|
||||
$scalarRedirect = "$apiBase/scalar/"
|
||||
$cfg = Read-DotEnv $envFile
|
||||
$issuer = $cfg['PUBLIC_AUTH_URL']
|
||||
$apiUrl = $cfg['PUBLIC_API_URL']
|
||||
$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.
|
||||
$deadline = (Get-Date).AddMinutes(3)
|
||||
@@ -41,7 +50,6 @@ function Invoke-Zitadel($method, $path, $body) {
|
||||
Invoke-RestMethod @params
|
||||
}
|
||||
|
||||
Write-Step "Waiting for Zitadel at $issuer"
|
||||
$deadline = (Get-Date).AddMinutes(2)
|
||||
do {
|
||||
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)
|
||||
if (-not $ready) { throw "Zitadel did not respond in time." }
|
||||
|
||||
# Login UI (idempotent) ------------------------------------------------------
|
||||
# The 'latest' Zitadel image requires the Login UI v2 (a separate 'login'
|
||||
# container that this stack does not run), so OIDC would redirect to
|
||||
# /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)"
|
||||
# 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.
|
||||
# Opting out keeps the built-in v1 login.
|
||||
try {
|
||||
Invoke-RestMethod -Method PUT -Uri "$issuer/v2/features/instance" -Headers $headers `
|
||||
-ContentType "application/json" -Body (@{ loginV2 = @{ required = $false } } | ConvertTo-Json) -TimeoutSec 20 | Out-Null
|
||||
Write-Host " loginV2.required = false"
|
||||
} 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) -------------------------------------------------------
|
||||
Write-Step "Project '$ProjectName'"
|
||||
# Project --------------------------------------------------------------------
|
||||
$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)"
|
||||
Write-Host " project '$ProjectName' reused (id $projectId)"
|
||||
} else {
|
||||
$projectId = (Invoke-Zitadel POST "/projects" @{ name = $ProjectName }).id
|
||||
Write-Host " created (id $projectId)"
|
||||
Write-Host " project '$ProjectName' created (id $projectId)"
|
||||
}
|
||||
|
||||
# OIDC app (idempotent) ------------------------------------------------------
|
||||
Write-Step "OIDC app '$AppName'"
|
||||
$app = (Invoke-Zitadel POST "/projects/$projectId/apps/_search" @{
|
||||
queries = @(@{ nameQuery = @{ name = $AppName; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
||||
}).result | Where-Object { $_.name -eq $AppName } | Select-Object -First 1
|
||||
function Get-ZitadelApp($name) {
|
||||
(Invoke-Zitadel POST "/projects/$projectId/apps/_search" @{
|
||||
queries = @(@{ nameQuery = @{ name = $name; method = "TEXT_QUERY_METHOD_EQUALS" } })
|
||||
}).result | Where-Object { $_.name -eq $name } | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ($app) {
|
||||
$clientId = (Invoke-Zitadel GET "/projects/$projectId/apps/$($app.id)" $null).app.oidcConfig.clientId
|
||||
Write-Host " reused (clientId $clientId)"
|
||||
# API app: public client used by the Scalar UI to obtain a token -------------
|
||||
$apiApp = Get-ZitadelApp $ApiAppName
|
||||
if ($apiApp) {
|
||||
$apiClientId = (Invoke-Zitadel GET "/projects/$projectId/apps/$($apiApp.id)" $null).app.oidcConfig.clientId
|
||||
Write-Host " app '$ApiAppName' reused (clientId $apiClientId)"
|
||||
} else {
|
||||
$clientId = (Invoke-Zitadel POST "/projects/$projectId/apps/oidc" @{
|
||||
name = $AppName
|
||||
redirectUris = @($scalarRedirect, "$apiBase/scalar/oauth2-redirect.html")
|
||||
postLogoutRedirectUris = @($scalarRedirect)
|
||||
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 = $true # allow http redirect URIs
|
||||
accessTokenType = "OIDC_TOKEN_TYPE_JWT" # required so JwtBearer can validate
|
||||
$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 = $true # allow http redirect URIs
|
||||
accessTokenType = "OIDC_TOKEN_TYPE_JWT" # required so JwtBearer can validate it
|
||||
accessTokenRoleAssertion = $true
|
||||
idTokenRoleAssertion = $true
|
||||
idTokenUserinfoAssertion = $true
|
||||
}).clientId
|
||||
Write-Host " created (clientId $clientId)"
|
||||
Write-Host " app '$ApiAppName' created (clientId $apiClientId)"
|
||||
}
|
||||
|
||||
# Wire the config: user-secrets (local runs) + .env (docker runs) -------------
|
||||
Write-Step "Wiring Authentication into Musify.Api user-secrets"
|
||||
# Web app: confidential client used by the SvelteKit server ------------------
|
||||
$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:AudienceAddress" = $clientId
|
||||
"Authentication:ClientId" = $apiClientId
|
||||
"Authentication:AudienceAddress" = $apiClientId
|
||||
"Authentication:IssuerAddress" = $issuer
|
||||
"Authentication:MetadataAddress" = "$issuer/.well-known/openid-configuration"
|
||||
"Authentication:AuthorizationEndpoint" = "$issuer/oauth/v2/authorize?prompt=login"
|
||||
"Authentication:TokenEndpoint" = "$issuer/oauth/v2/token"
|
||||
"Authentication:ScalarRedirectUri" = $scalarRedirect
|
||||
"Authentication:ScalarRedirectUri" = "$apiUrl/scalar/"
|
||||
"Authentication:RequireHttpsMetadata" = "false"
|
||||
}.GetEnumerator() | ForEach-Object {
|
||||
dotnet user-secrets set $_.Key $_.Value --project $apiProject | Out-Null
|
||||
}
|
||||
|
||||
$envPath = Join-Path $deployDir ".env"
|
||||
$envLines = Get-Content $envPath
|
||||
if ($envLines -match '^\s*AUTH_CLIENT_ID=') {
|
||||
$envLines = $envLines -replace '^\s*AUTH_CLIENT_ID=.*', "AUTH_CLIENT_ID=$clientId"
|
||||
} else {
|
||||
$envLines += "AUTH_CLIENT_ID=$clientId"
|
||||
# web-player/.env is what `npm run dev` reads (the container gets its config
|
||||
# from compose instead).
|
||||
if (-not (Test-Path $webEnvFile)) {
|
||||
Copy-Item (Join-Path $repoDir "web-player\.env.example") $webEnvFile
|
||||
}
|
||||
@{
|
||||
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 " Project: $ProjectName ($projectId)"
|
||||
Write-Host " ClientId: $clientId"
|
||||
Write-Host " Issuer: $issuer"
|
||||
Write-Host " wired into deploy/.env, Musify.Api user-secrets and web-player/.env"
|
||||
|
||||
Reference in New Issue
Block a user