feat(deploy): add production stack for the VPS
Compose file for ikerdemo.cat with nginx as TLS terminator, the three .NET services, Zitadel and the storage stack. Values come from deploy/.env.prod (untracked). Adds the frontend image, the migration script and the Zitadel provisioning for production.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
#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)" }
|
||||
Reference in New Issue
Block a user