Requested: dev-token.sh/.ps1 tokens should last 3h by default, with an argument to override the duration. Server-verified finding: STALWART_RECOVERY_ADMIN (the break-glass account docker-compose.yml and the scripts used) always issues OAuth tokens with a fixed 1h expiry, regardless of the server's configured accessTokenExpiry — confirmed against the live container, including after changing the setting and restarting. Confirmed x:ApiKey objects, by contrast, support an arbitrary expiresAt set per request, and their secret works directly as a bearer token. Add scripts/dev-server-init.sh (+ .ps1): a one-time, idempotent setup step that completes the server's bootstrap wizard (default domain, no TLS certificate request), creates a real "devadmin" admin account, and sets the server's default OAuth token lifetime to 3h. Rework dev-token.sh/.ps1 to authenticate as devadmin and create an x:ApiKey with a caller-supplied expiry (`dev-token.sh 1800` for 30 minutes, defaults to 10800s/3h) instead of running the OAuth PKCE flow against the recovery account. Verified end-to-end against a fresh container, including a real browser session against the running WebUI. Also includes an incidental package-lock.json sync (was still pinned to v1.0.8 / stale dependency ranges from before the upstream merge).
123 lines
4.7 KiB
PowerShell
123 lines
4.7 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 {}
|
|
|
|
Write-Host "Done. $DevAdminName@$DevDomain / $DevAdminSecret is ready - run scripts/dev-token.ps1 to get a token."
|