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).
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#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."
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/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
|
||||
+47
-36
@@ -1,60 +1,71 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generates a fresh OAuth access token from the local Stalwart dev container
|
||||
and writes it to .env.development.local (gitignored).
|
||||
Generates a fresh access token from the local Stalwart dev container and
|
||||
writes it to .env.development.local (gitignored).
|
||||
|
||||
.DESCRIPTION
|
||||
Local development only. Requires the dev container from docker-compose.yml
|
||||
(`docker compose up -d`) to be running. Tokens expire after 1 hour; re-run
|
||||
this script and restart "npm run dev" when the UI starts returning 401s.
|
||||
The credentials below belong to the disposable local Stalwart container.
|
||||
See DEVELOPMENT.md for the full workflow. Non-Windows shells (and AI
|
||||
agents without PowerShell) can use scripts/dev-token.sh instead.
|
||||
Local development only. Requires scripts/dev-server-init.ps1 to have been
|
||||
run once first (creates the "devadmin" account this script authenticates
|
||||
as — the STALWART_RECOVERY_ADMIN account is break-glass only and always
|
||||
issues fixed 1h tokens regardless of server config, so it can't honor a
|
||||
custom duration). See DEVELOPMENT.md for the full workflow. Non-Windows
|
||||
shells (and AI agents without PowerShell) can use scripts/dev-token.sh
|
||||
instead.
|
||||
|
||||
.PARAMETER DurationSeconds
|
||||
How long the token should stay valid, in seconds. Defaults to 3 hours
|
||||
(10800), matching the server default set by dev-server-init.ps1.
|
||||
|
||||
.EXAMPLE
|
||||
./dev-token.ps1 # 3 hour token
|
||||
.EXAMPLE
|
||||
./dev-token.ps1 -DurationSeconds 1800 # 30 minute token
|
||||
#>
|
||||
param(
|
||||
[string]$ApiBaseUrl = "http://localhost:8080",
|
||||
[string]$AccountName = "admin@example.org",
|
||||
[string]$AccountSecret = "c8321iEscHDy0GWV"
|
||||
[int]$DurationSeconds = 10800,
|
||||
[string]$ApiBaseUrl = "http://localhost:8080"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
# PKCE pair (S256)
|
||||
$chars = (48..57) + (65..90) + (97..122)
|
||||
$verifier = -join ($chars | Get-Random -Count 64 | ForEach-Object { [char]$_ })
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
$challenge = [Convert]::ToBase64String($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($verifier))).Replace('+', '-').Replace('/', '_').TrimEnd('=')
|
||||
$DevAdminAccount = "devadmin@example.org"
|
||||
$DevAdminSecret = "DevAdminPass123!"
|
||||
|
||||
$redirectUri = "http://localhost:3005/oauth/callback"
|
||||
$creds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($DevAdminAccount):$($DevAdminSecret)"))
|
||||
$authHeader = @{ Authorization = "Basic $creds" }
|
||||
|
||||
$authPayload = @{
|
||||
type = "authCode"
|
||||
accountName = $AccountName
|
||||
accountSecret = $AccountSecret
|
||||
clientId = "stalwart-webui"
|
||||
redirectUri = $redirectUri
|
||||
scope = "openid email profile offline_access"
|
||||
state = [guid]::NewGuid().ToString("N")
|
||||
codeChallenge = $challenge
|
||||
codeChallengeMethod = "S256"
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
$auth = Invoke-RestMethod -Uri "$ApiBaseUrl/api/auth" -Method Post -ContentType "application/json" -Body $authPayload -TimeoutSec 15
|
||||
if ($auth.type -ne "authenticated" -or -not $auth.client_code) {
|
||||
throw "Unexpected /api/auth response: $($auth | ConvertTo-Json -Compress)"
|
||||
try {
|
||||
$session = Invoke-RestMethod -Uri "$ApiBaseUrl/jmap/session" -Headers $authHeader -TimeoutSec 15
|
||||
} catch {
|
||||
throw "Could not reach $ApiBaseUrl as $DevAdminAccount. Is the server running ('npm run dev:server') and initialized ('scripts/dev-server-init.ps1')?"
|
||||
}
|
||||
$accountId = $session.primaryAccounts.'urn:stalwart:jmap'
|
||||
|
||||
$tokenBody = "grant_type=authorization_code&code=$($auth.client_code)&code_verifier=$verifier&client_id=stalwart-webui&redirect_uri=$([uri]::EscapeDataString($redirectUri))"
|
||||
$token = Invoke-RestMethod -Uri "$ApiBaseUrl/auth/token" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $tokenBody -TimeoutSec 15
|
||||
$expiresAt = [DateTime]::UtcNow.AddSeconds($DurationSeconds).ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
|
||||
$request = @{
|
||||
using = @("urn:ietf:params:jmap:core", "urn:stalwart:jmap")
|
||||
methodCalls = @(, @("x:ApiKey/set", @{
|
||||
accountId = $accountId
|
||||
create = @{ k1 = @{ description = "dev-token.ps1"; expiresAt = $expiresAt } }
|
||||
}, "0"))
|
||||
} | ConvertTo-Json -Depth 10 -Compress
|
||||
|
||||
$response = Invoke-RestMethod -Uri "$ApiBaseUrl/jmap/" -Method Post -ContentType "application/json" -Headers $authHeader -Body $request -TimeoutSec 15
|
||||
$secret = $response.methodResponses[0][1].created.k1.secret
|
||||
|
||||
if (-not $secret) {
|
||||
throw "Unexpected x:ApiKey/set response: $($response | ConvertTo-Json -Depth 10 -Compress)"
|
||||
}
|
||||
|
||||
$envPath = Join-Path $root ".env.development.local"
|
||||
@"
|
||||
# Generated by scripts/dev-token.ps1 - gitignored, do not commit.
|
||||
# Empty base URL: API calls stay same-origin and go through the Vite proxy.
|
||||
VITE_API_BASE_URL=
|
||||
VITE_ACCESS_TOKEN=$($token.access_token)
|
||||
VITE_ACCESS_TOKEN=$secret
|
||||
"@ | Set-Content -Path $envPath -Encoding ascii
|
||||
|
||||
Write-Host "Token written to $envPath (expires in $($token.expires_in)s). Restart 'npm run dev' to pick it up."
|
||||
Write-Host "Token written to $envPath (expires $expiresAt, in ${DurationSeconds}s). Restart 'npm run dev' to pick it up."
|
||||
|
||||
+32
-38
@@ -1,54 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local development only. Generates a fresh OAuth access token from the local
|
||||
# Local development only. Generates a fresh access token from the local
|
||||
# Stalwart dev container (see docker-compose.yml) and writes it to
|
||||
# .env.development.local (gitignored). Bash equivalent of dev-token.ps1, for
|
||||
# non-Windows shells (and AI agents without PowerShell).
|
||||
#
|
||||
# Tokens expire after 1 hour; re-run this script and restart "npm run dev"
|
||||
# when the UI starts returning 401s.
|
||||
# The credentials below belong to the disposable local Stalwart container.
|
||||
# Requires scripts/dev-server-init.sh to have been run once first (creates
|
||||
# the "devadmin" account this script authenticates as — the
|
||||
# STALWART_RECOVERY_ADMIN account is break-glass only and always issues
|
||||
# fixed 1h tokens regardless of server config, so it can't honor a custom
|
||||
# duration).
|
||||
#
|
||||
# Usage: dev-token.sh [duration_seconds] [api_base_url]
|
||||
# dev-token.sh # 3 hour token (server default, see dev-server-init.sh)
|
||||
# dev-token.sh 1800 # 30 minute token
|
||||
set -euo pipefail
|
||||
|
||||
API_BASE_URL="${1:-http://localhost:8080}"
|
||||
ACCOUNT_NAME="${2:-admin@example.org}"
|
||||
ACCOUNT_SECRET="${3:-c8321iEscHDy0GWV}"
|
||||
REDIRECT_URI="http://localhost:3005/oauth/callback"
|
||||
DURATION_SECONDS="${1:-10800}"
|
||||
API_BASE_URL="${2:-http://localhost:8080}"
|
||||
DEVADMIN_ACCOUNT="devadmin@example.org"
|
||||
DEVADMIN_SECRET="DevAdminPass123!"
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
b64url() {
|
||||
base64 | tr '+/' '-_' | tr -d '=\n'
|
||||
if ! date -u -d "+1 minute" +"%Y-%m-%dT%H:%M:%SZ" >/dev/null 2>&1; then
|
||||
EXPIRES_AT=$(date -u -v+"${DURATION_SECONDS}"S +"%Y-%m-%dT%H:%M:%SZ") # BSD/macOS date
|
||||
else
|
||||
EXPIRES_AT=$(date -u -d "+${DURATION_SECONDS} seconds" +"%Y-%m-%dT%H:%M:%SZ") # GNU date
|
||||
fi
|
||||
|
||||
SESSION=$(curl -sf --compressed -u "$DEVADMIN_ACCOUNT:$DEVADMIN_SECRET" "$API_BASE_URL/jmap/session") || {
|
||||
echo "Could not reach $API_BASE_URL as $DEVADMIN_ACCOUNT. Is the server running ('npm run dev:server') and initialized ('scripts/dev-server-init.sh')?" >&2
|
||||
exit 1
|
||||
}
|
||||
ACCOUNT_ID=$(printf '%s' "$SESSION" | grep -o '"urn:stalwart:jmap":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
VERIFIER="$(head -c 48 /dev/urandom | b64url | head -c 64)"
|
||||
CHALLENGE="$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | b64url)"
|
||||
STATE="$(head -c 16 /dev/urandom | xxd -p)"
|
||||
|
||||
AUTH_PAYLOAD=$(cat <<JSON
|
||||
{"type":"authCode","accountName":"$ACCOUNT_NAME","accountSecret":"$ACCOUNT_SECRET","clientId":"stalwart-webui","redirectUri":"$REDIRECT_URI","scope":"openid email profile offline_access","state":"$STATE","codeChallenge":"$CHALLENGE","codeChallengeMethod":"S256"}
|
||||
REQ=$(cat <<JSON
|
||||
{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:ApiKey/set",{"accountId":"$ACCOUNT_ID","create":{"k1":{"description":"dev-token.sh","expiresAt":"$EXPIRES_AT"}}},"0"]]}
|
||||
JSON
|
||||
)
|
||||
|
||||
AUTH_RESPONSE=$(curl -sf "$API_BASE_URL/api/auth" -X POST -H "Content-Type: application/json" -d "$AUTH_PAYLOAD")
|
||||
CLIENT_CODE=$(printf '%s' "$AUTH_RESPONSE" | grep -o '"client_code":"[^"]*"' | cut -d'"' -f4)
|
||||
RESPONSE=$(curl -sf --compressed -u "$DEVADMIN_ACCOUNT:$DEVADMIN_SECRET" -X POST -H "Content-Type: application/json" -d "$REQ" "$API_BASE_URL/jmap/")
|
||||
TOKEN=$(printf '%s' "$RESPONSE" | grep -o '"secret":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$CLIENT_CODE" ]; then
|
||||
echo "Unexpected /api/auth response: $AUTH_RESPONSE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN_RESPONSE=$(curl -sf "$API_BASE_URL/auth/token" -X POST \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data-urlencode "grant_type=authorization_code" \
|
||||
--data-urlencode "code=$CLIENT_CODE" \
|
||||
--data-urlencode "code_verifier=$VERIFIER" \
|
||||
--data-urlencode "client_id=stalwart-webui" \
|
||||
--data-urlencode "redirect_uri=$REDIRECT_URI")
|
||||
|
||||
ACCESS_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
|
||||
EXPIRES_IN=$(printf '%s' "$TOKEN_RESPONSE" | grep -o '"expires_in":[0-9]*' | cut -d':' -f2)
|
||||
|
||||
if [ -z "$ACCESS_TOKEN" ]; then
|
||||
echo "Unexpected /auth/token response: $TOKEN_RESPONSE" >&2
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "Unexpected x:ApiKey/set response: $RESPONSE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -57,7 +51,7 @@ cat > "$ENV_PATH" <<EOF
|
||||
# Generated by scripts/dev-token.sh - gitignored, do not commit.
|
||||
# Empty base URL: API calls stay same-origin and go through the Vite proxy.
|
||||
VITE_API_BASE_URL=
|
||||
VITE_ACCESS_TOKEN=$ACCESS_TOKEN
|
||||
VITE_ACCESS_TOKEN=$TOKEN
|
||||
EOF
|
||||
|
||||
echo "Token written to $ENV_PATH (expires in ${EXPIRES_IN}s). Restart 'npm run dev' to pick it up."
|
||||
echo "Token written to $ENV_PATH (expires $EXPIRES_AT, in ${DURATION_SECONDS}s). Restart 'npm run dev' to pick it up."
|
||||
|
||||
Reference in New Issue
Block a user