Files
Stalwart-webui/scripts/dev-server-init.sh
T
Steven RYDELL adca6d8730 feat(dev): switch dev tokens to a real admin account with configurable duration
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).
2026-08-01 18:32:13 +02:00

66 lines
4.0 KiB
Bash

#!/usr/bin/env bash
# Local development only. One-time setup for the disposable Stalwart test
# server from docker-compose.yml: completes the server's bootstrap wizard,
# creates a real "devadmin" account (used by dev-token.sh/.ps1 — the
# STALWART_RECOVERY_ADMIN account is break-glass only and always issues
# fixed 1h OAuth tokens regardless of server config), and sets the default
# OAuth access token lifetime to 3 hours.
#
# Idempotent: safe to re-run; does nothing if the server already left
# bootstrap mode. Run this once after `npm run dev:server` on a fresh
# volume (or after `docker compose down -v`).
set -euo pipefail
API_BASE_URL="${1:-http://localhost:8080}"
RECOVERY_ACCOUNT="admin@example.org"
RECOVERY_SECRET="c8321iEscHDy0GWV"
DEV_DOMAIN="example.org"
DEV_HOSTNAME="mail.example.org"
DEVADMIN_NAME="devadmin"
DEVADMIN_SECRET="DevAdminPass123!"
DEFAULT_TOKEN_EXPIRY_MS=10800000 # 3 hours
jmap() {
curl -sf --compressed -u "$RECOVERY_ACCOUNT:$RECOVERY_SECRET" \
-X POST -H "Content-Type: application/json" -d "$1" "$API_BASE_URL/jmap/"
}
echo "Waiting for $API_BASE_URL to be reachable..."
for _ in $(seq 1 30); do
curl -sf -o /dev/null -u "$RECOVERY_ACCOUNT:$RECOVERY_SECRET" "$API_BASE_URL/jmap/session" && break
sleep 1
done
SESSION=$(curl -sf --compressed -u "$RECOVERY_ACCOUNT:$RECOVERY_SECRET" "$API_BASE_URL/jmap/session")
ACCOUNT_ID=$(printf '%s' "$SESSION" | grep -o '"urn:stalwart:jmap":"[^"]*"' | cut -d'"' -f4)
QUERY_RESULT=$(jmap "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":[[\"x:Domain/query\",{\"accountId\":\"$ACCOUNT_ID\"},\"0\"]]}")
if ! printf '%s' "$QUERY_RESULT" | grep -q '"forbidden"'; then
echo "Server already bootstrapped, skipping setup. (Use 'docker compose down -v && npm run dev:server' to start fresh.)"
else
echo "Completing server bootstrap (domain: $DEV_DOMAIN, no TLS certificate request)..."
jmap "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":[[\"x:Bootstrap/set\",{\"accountId\":\"$ACCOUNT_ID\",\"update\":{\"singleton\":{\"defaultDomain\":\"$DEV_DOMAIN\",\"serverHostname\":\"$DEV_HOSTNAME\",\"requestTlsCertificate\":false}}},\"0\"]]}" > /dev/null
echo "Restarting the container to apply bootstrap config (one-time only)..."
docker compose restart stalwart > /dev/null
for _ in $(seq 1 30); do
curl -sf -o /dev/null -u "$RECOVERY_ACCOUNT:$RECOVERY_SECRET" "$API_BASE_URL/jmap/session" && break
sleep 1
done
DOMAIN_RESULT=$(jmap "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":[[\"x:Domain/query\",{\"accountId\":\"$ACCOUNT_ID\"},\"0\"]]}")
DOMAIN_ID=$(printf '%s' "$DOMAIN_RESULT" | grep -o '"ids":\["[^"]*"' | cut -d'"' -f4)
echo "Creating devadmin account ($DEVADMIN_NAME@$DEV_DOMAIN)..."
CREATE_RESULT=$(jmap "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":[[\"x:Account/set\",{\"accountId\":\"$ACCOUNT_ID\",\"create\":{\"u1\":{\"@type\":\"User\",\"name\":\"$DEVADMIN_NAME\",\"domainId\":\"$DOMAIN_ID\",\"roles\":{\"@type\":\"Admin\"}}}},\"0\"]]}")
DEVADMIN_ID=$(printf '%s' "$CREATE_RESULT" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
jmap "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":[[\"x:Account/set\",{\"accountId\":\"$ACCOUNT_ID\",\"update\":{\"$DEVADMIN_ID\":{\"credentials\":{\"0\":{\"@type\":\"Password\",\"secret\":\"$DEVADMIN_SECRET\"}}}}},\"0\"]]}" > /dev/null
echo "Setting default OAuth access token lifetime to 3 hours..."
jmap "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":[[\"x:OidcProvider/set\",{\"accountId\":\"$ACCOUNT_ID\",\"update\":{\"singleton\":{\"accessTokenExpiry\":$DEFAULT_TOKEN_EXPIRY_MS}}},\"0\"]]}" > /dev/null
jmap "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":[[\"x:Action/set\",{\"accountId\":\"$ACCOUNT_ID\",\"create\":{\"a1\":{\"@type\":\"ReloadSettings\"}}},\"0\"]]}" > /dev/null 2>&1 || true
echo "Done. devadmin@$DEV_DOMAIN / $DEVADMIN_SECRET is ready — run scripts/dev-token.sh to get a token."
fi