Files
Stalwart-webui/scripts/dev-server-init.ps1
T
Steven RYDELL 40164a6652 feat(webapps): show and correctly set the active WebUI's resource URL
The "Active WebUI" card (Settings > Web Applications) only showed
description and __APP_VERSION__, never the resourceUrl it's actually
built from — add it as a linked "Source" row.

That card also turned out to be misleading in local dev: it reads the
x:Application record whose urlPrefix matches /admin or /account, and a
freshly bootstrapped server keeps Stalwart's seeded default there
("Stalwart Web Interface", pointing at stalwartlabs/webui's release),
regardless of what's actually being served — in dev that's this fork,
served directly by Vite, which never touches that record at all.

dev-server-init.sh/.ps1 now point that record's description and
resourceUrl at this fork's own release, so the card (now including the
visible Source URL) reflects what's actually running instead of
Stalwart's factory default.

Verified end-to-end against a fresh container with both scripts.
2026-08-01 19:17:44 +02:00

148 lines
5.8 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
One-time setup for the disposable Stalwart test server from
docker-compose.yml: completes the bootstrap wizard, creates a real
"devadmin" account, and sets the default OAuth access token lifetime.
.DESCRIPTION
Local development only. Requires the dev container to be running
('npm run dev:server'). The STALWART_RECOVERY_ADMIN account is
break-glass only and always issues fixed 1h OAuth tokens regardless of
server config, so dev-token.ps1 authenticates as "devadmin" instead,
created here. Idempotent: safe to re-run; does nothing if the server
already left bootstrap mode.
#>
param(
[string]$ApiBaseUrl = "http://localhost:8080"
)
$ErrorActionPreference = 'Stop'
$RecoveryAccount = "admin@example.org"
$RecoverySecret = "c8321iEscHDy0GWV"
$DevDomain = "example.org"
$DevHostname = "mail.example.org"
$DevAdminName = "devadmin"
$DevAdminSecret = "DevAdminPass123!"
$DefaultTokenExpiryMs = 10800000 # 3 hours
$recoveryCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($RecoveryAccount):$($RecoverySecret)"))
$authHeader = @{ Authorization = "Basic $recoveryCreds" }
function Invoke-Jmap($body) {
Invoke-RestMethod -Uri "$ApiBaseUrl/jmap/" -Method Post -ContentType "application/json" -Headers $authHeader -Body ($body | ConvertTo-Json -Depth 10 -Compress) -TimeoutSec 15
}
Write-Host "Waiting for $ApiBaseUrl to be reachable..."
$ready = $false
for ($i = 0; $i -lt 30; $i++) {
try {
Invoke-RestMethod -Uri "$ApiBaseUrl/jmap/session" -Headers $authHeader -TimeoutSec 5 | Out-Null
$ready = $true
break
} catch { Start-Sleep -Seconds 1 }
}
if (-not $ready) { throw "Server did not become reachable at $ApiBaseUrl" }
$session = Invoke-RestMethod -Uri "$ApiBaseUrl/jmap/session" -Headers $authHeader -TimeoutSec 15
$accountId = $session.primaryAccounts.'urn:stalwart:jmap'
$queryResult = Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Domain/query", @{ accountId = $accountId }, "0"))
}
$alreadyBootstrapped = -not ($queryResult.methodResponses[0][1].type -eq "forbidden")
if ($alreadyBootstrapped) {
Write-Host "Server already bootstrapped, skipping setup. (Use 'docker compose down -v; npm run dev:server' to start fresh.)"
return
}
Write-Host "Completing server bootstrap (domain: $DevDomain, no TLS certificate request)..."
Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Bootstrap/set", @{
accountId = $accountId
update = @{ singleton = @{ defaultDomain = $DevDomain; serverHostname = $DevHostname; requestTlsCertificate = $false } }
}, "0"))
} | Out-Null
Write-Host "Restarting the container to apply bootstrap config (one-time only)..."
docker compose restart stalwart | Out-Null
$ready = $false
for ($i = 0; $i -lt 30; $i++) {
try {
Invoke-RestMethod -Uri "$ApiBaseUrl/jmap/session" -Headers $authHeader -TimeoutSec 5 | Out-Null
$ready = $true
break
} catch { Start-Sleep -Seconds 1 }
}
if (-not $ready) { throw "Server did not come back up after restart" }
$domainResult = Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Domain/query", @{ accountId = $accountId }, "0"))
}
$domainId = $domainResult.methodResponses[0][1].ids[0]
Write-Host "Creating devadmin account ($DevAdminName@$DevDomain)..."
$createResult = Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Account/set", @{
accountId = $accountId
create = @{ u1 = @{ "@type" = "User"; name = $DevAdminName; domainId = $domainId; roles = @{ "@type" = "Admin" } } }
}, "0"))
}
$devAdminId = $createResult.methodResponses[0][1].created.u1.id
Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Account/set", @{
accountId = $accountId
update = @{ $devAdminId = @{ credentials = @{ "0" = @{ "@type" = "Password"; secret = $DevAdminSecret } } } }
}, "0"))
} | Out-Null
Write-Host "Setting default OAuth access token lifetime to 3 hours..."
Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:OidcProvider/set", @{
accountId = $accountId
update = @{ singleton = @{ accessTokenExpiry = $DefaultTokenExpiryMs } }
}, "0"))
} | Out-Null
try {
Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Action/set", @{ accountId = $accountId; create = @{ a1 = @{ "@type" = "ReloadSettings" } } }, "0"))
} | Out-Null
} catch {}
# Cosmetic only: Stalwart seeds a default "Stalwart Web Interface" /
# stalwartlabs/webui entry for the x:Application record serving /admin and
# /account (see Settings > Web Applications' "Active WebUI" card). Point it
# at this fork so that card isn't misleading in local dev too.
Write-Host "Pointing the active WebUI's description at this fork..."
try {
$appResult = Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Application/query", @{ accountId = $accountId }, "0"))
}
$appId = $appResult.methodResponses[0][1].ids[0]
if ($appId) {
Invoke-Jmap @{
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
methodCalls = @(, @("x:Application/set", @{
accountId = $accountId
update = @{ $appId = @{
description = "Stalwart WebUI Fork"
resourceUrl = "https://github.com/LinkPhoenix/stalwart-webui-fork/releases/latest/download/webui.zip"
} }
}, "0"))
} | Out-Null
}
} catch {}
Write-Host "Done. $DevAdminName@$DevDomain / $DevAdminSecret is ready - run scripts/dev-token.ps1 to get a token."