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.
148 lines
5.8 KiB
PowerShell
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."
|