feat(dev): add one-command local Stalwart test server + workflow docs
Add docker-compose.yml: a disposable Stalwart instance with a fixed dev admin account (STALWART_RECOVERY_ADMIN), matching the credentials scripts/dev-token.ps1 already expected. Wired up via new npm scripts (dev:server, dev:server:down, dev:server:logs). scripts/dev-token.ps1 was previously untracked (the whole scripts/ directory was gitignored) even though it's part of the documented dev workflow — un-ignored it, and added scripts/dev-token.sh, a POSIX equivalent for non-Windows shells and AI agents without PowerShell. New DEVELOPMENT.md documents the full loop end-to-end (start server, get a token, run the dev server, verify), written so it's actionable by both humans and AI coding agents without needing a browser. Linked from AGENTS.md (Commands) and README.md (Getting started). Verified manually: docker compose up brings the server to a healthy state, /api/auth + /auth/token issue a working bearer token, and /jmap/session returns 200 with it end-to-end.
This commit is contained in:
+4
-1
@@ -23,13 +23,16 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.ignore
|
.ignore
|
||||||
scripts/
|
scripts/*
|
||||||
|
!scripts/dev-token.ps1
|
||||||
|
!scripts/dev-token.sh
|
||||||
*.md
|
*.md
|
||||||
!README.md
|
!README.md
|
||||||
!CHANGELOG.md
|
!CHANGELOG.md
|
||||||
!AGENTS.md
|
!AGENTS.md
|
||||||
!CLAUDE.md
|
!CLAUDE.md
|
||||||
!SCHEMA_DEVIATIONS.md
|
!SCHEMA_DEVIATIONS.md
|
||||||
|
!DEVELOPMENT.md
|
||||||
!.agents/rules/*.md
|
!.agents/rules/*.md
|
||||||
/SPEC-*
|
/SPEC-*
|
||||||
# Tool-generated artifacts during agent sessions
|
# Tool-generated artifacts during agent sessions
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ Community fork of [stalwartlabs/webui](https://github.com/stalwartlabs/webui), a
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- `npm run dev` - Dev server
|
- `npm run dev:server` - Start a local disposable Stalwart test server (Docker); `npm run dev:server:down` to stop it
|
||||||
|
- `bash scripts/dev-token.sh` (or `pwsh ./scripts/dev-token.ps1` on Windows) - Get a dev access token from that server
|
||||||
|
- `npm run dev` - Dev server (proxies `/api` and `/jmap` to the test server)
|
||||||
- `npm run typecheck` - `tsc --noEmit`
|
- `npm run typecheck` - `tsc --noEmit`
|
||||||
- `npm run lint` - ESLint
|
- `npm run lint` - ESLint
|
||||||
- `npm run test` - Vitest (`npm run test:watch` to watch)
|
- `npm run test` - Vitest (`npm run test:watch` to watch)
|
||||||
- `npm run build` - `tsc -b && vite build`
|
- `npm run build` - `tsc -b && vite build`
|
||||||
|
|
||||||
|
Full local dev workflow, including how to run it end-to-end without a browser: [DEVELOPMENT.md](DEVELOPMENT.md).
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
The detailed rules live in `.agents/rules/`. Read the relevant file before acting:
|
The detailed rules live in `.agents/rules/`. Read the relevant file before acting:
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
How to run a local Stalwart test server and develop this WebUI against it.
|
||||||
|
This file is meant to be read by humans and AI coding agents alike — see
|
||||||
|
[AGENTS.md](AGENTS.md) for the rules that also apply while doing this.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- Docker (with the `docker compose` CLI plugin)
|
||||||
|
|
||||||
|
## 1. Start a local test server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:server
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs `docker compose up -d`, which starts a disposable Stalwart
|
||||||
|
instance ([`docker-compose.yml`](docker-compose.yml)) with:
|
||||||
|
|
||||||
|
- The management/JMAP HTTP API on `http://localhost:8080` (the only port
|
||||||
|
the WebUI dev proxy needs — see `server.proxy` in
|
||||||
|
[`vite.config.ts`](vite.config.ts)).
|
||||||
|
- Mail protocol ports (SMTP/IMAP/POP3/ManageSieve) exposed too, only
|
||||||
|
needed if you're testing actual mail flows, not just admin UI screens.
|
||||||
|
- A fixed admin account baked in via `STALWART_RECOVERY_ADMIN`:
|
||||||
|
`admin@example.org` / `c8321iEscHDy0GWV`. **Disposable dev credentials
|
||||||
|
only — never reuse them for anything real.**
|
||||||
|
- Named Docker volumes (`stalwart-etc`, `stalwart-data`) so data survives
|
||||||
|
a restart. Data persists until you tear the volumes down.
|
||||||
|
|
||||||
|
Useful companions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:server:logs # tail the container's logs
|
||||||
|
npm run dev:server:down # stop it (add `-v` via `docker compose down -v` to also wipe data)
|
||||||
|
```
|
||||||
|
|
||||||
|
The server takes a couple of seconds to come up; `docker compose logs stalwart`
|
||||||
|
will show `Network listener started ... localPort = 8080` once it's ready.
|
||||||
|
|
||||||
|
## 2. Get an access token
|
||||||
|
|
||||||
|
The WebUI normally authenticates through an OAuth flow in the browser, but
|
||||||
|
for local development it's simpler to skip that and use a bearer token
|
||||||
|
directly via `VITE_ACCESS_TOKEN` (see `.env.development`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows / PowerShell
|
||||||
|
pwsh ./scripts/dev-token.ps1
|
||||||
|
|
||||||
|
# Linux / macOS / any POSIX shell (including most AI agent sandboxes)
|
||||||
|
bash ./scripts/dev-token.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Both scripts log in as the dev container's admin account, run the full
|
||||||
|
OAuth PKCE flow against it, and write the resulting token to
|
||||||
|
`.env.development.local` (gitignored, never committed). Tokens expire
|
||||||
|
after 1 hour — re-run the script and restart `npm run dev` if the UI
|
||||||
|
starts returning 401s.
|
||||||
|
|
||||||
|
## 3. Run the WebUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # first time only
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:5173`. You should land directly in the admin panel
|
||||||
|
(no login screen) since `VITE_ACCESS_TOKEN` is set.
|
||||||
|
|
||||||
|
## 4. Verify your change
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
npm run lint
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
For UI changes, actually look at the running app (browser or a browser
|
||||||
|
automation tool) — passing typecheck/lint/tests proves the code compiles
|
||||||
|
and existing behavior didn't regress, it doesn't prove the new UI works.
|
||||||
|
|
||||||
|
## Resetting the test server
|
||||||
|
|
||||||
|
To start from a completely clean server (e.g. to re-test first-run
|
||||||
|
behavior):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev:server:down
|
||||||
|
docker compose down -v # also removes the stalwart-etc/stalwart-data volumes
|
||||||
|
npm run dev:server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes for AI agents
|
||||||
|
|
||||||
|
- This whole workflow (steps 1–3) is scriptable end-to-end without a
|
||||||
|
browser: `npm run dev:server`, then `bash scripts/dev-token.sh`, then
|
||||||
|
the app is reachable at `http://localhost:5173` with
|
||||||
|
`VITE_ACCESS_TOKEN` already set. Verify backend connectivity directly
|
||||||
|
with `curl`, e.g. `curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/jmap/session`.
|
||||||
|
- Read [AGENTS.md](AGENTS.md) before touching anything under `src/` —
|
||||||
|
the schema-fidelity rule applies to all development, local test server
|
||||||
|
or not.
|
||||||
|
- Don't commit `.env.development.local` (it holds a live token) or leave
|
||||||
|
the dev container running unexpectedly — `npm run dev:server:down` when
|
||||||
|
you're done.
|
||||||
@@ -118,7 +118,7 @@ Every tagged release of this fork publishes a `webui.zip` build via CI (see [`.g
|
|||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
- Node.js 18 or later
|
- Node.js 18 or later
|
||||||
- A running Stalwart instance (for JMAP API calls)
|
- A running Stalwart instance (for JMAP API calls) — see [DEVELOPMENT.md](DEVELOPMENT.md) for how to spin up a disposable local test server with Docker in one command, no manual Stalwart setup required.
|
||||||
|
|
||||||
Install dependencies:
|
Install dependencies:
|
||||||
|
|
||||||
@@ -146,12 +146,14 @@ VITE_OAUTH_SCOPES=
|
|||||||
|
|
||||||
### Bypassing OAuth for development
|
### Bypassing OAuth for development
|
||||||
|
|
||||||
Set `VITE_ACCESS_TOKEN` to a valid bearer token to skip the login page and go straight to the admin panel. You can obtain a token from the Stalwart server's token endpoint or use an API key:
|
Set `VITE_ACCESS_TOKEN` to a valid bearer token to skip the login page and go straight to the admin panel:
|
||||||
|
|
||||||
```
|
```
|
||||||
VITE_ACCESS_TOKEN=your-bearer-token-here
|
VITE_ACCESS_TOKEN=your-bearer-token-here
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Against the local test server from [DEVELOPMENT.md](DEVELOPMENT.md), `scripts/dev-token.ps1` / `scripts/dev-token.sh` fetch one for you automatically.
|
||||||
|
|
||||||
### Running the dev server
|
### Running the dev server
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
stalwart:
|
||||||
|
image: stalwartlabs/stalwart:latest
|
||||||
|
container_name: stalwart-webui-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
# HTTP management/JMAP API — the only port the WebUI dev proxy needs
|
||||||
|
# (see server.proxy in vite.config.ts).
|
||||||
|
- "8080:8080"
|
||||||
|
# Mail protocols, only needed if you're testing actual mail flows
|
||||||
|
# (sending/receiving, not just admin UI screens).
|
||||||
|
- "25:25"
|
||||||
|
- "587:587"
|
||||||
|
- "465:465"
|
||||||
|
- "143:143"
|
||||||
|
- "993:993"
|
||||||
|
- "110:110"
|
||||||
|
- "995:995"
|
||||||
|
- "4190:4190"
|
||||||
|
volumes:
|
||||||
|
- stalwart-etc:/etc/stalwart
|
||||||
|
- stalwart-data:/var/lib/stalwart
|
||||||
|
environment:
|
||||||
|
# Disposable local dev credentials — do not reuse for anything real.
|
||||||
|
# Matches scripts/dev-token.ps1's defaults.
|
||||||
|
STALWART_RECOVERY_ADMIN: "admin@example.org:c8321iEscHDy0GWV"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
stalwart-etc:
|
||||||
|
stalwart-data:
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"dev:server": "docker compose up -d",
|
||||||
|
"dev:server:down": "docker compose down",
|
||||||
|
"dev:server:logs": "docker compose logs -f stalwart",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#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).
|
||||||
|
|
||||||
|
.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.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string]$ApiBaseUrl = "http://localhost:8080",
|
||||||
|
[string]$AccountName = "admin@example.org",
|
||||||
|
[string]$AccountSecret = "c8321iEscHDy0GWV"
|
||||||
|
)
|
||||||
|
|
||||||
|
$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('=')
|
||||||
|
|
||||||
|
$redirectUri = "http://localhost:3005/oauth/callback"
|
||||||
|
|
||||||
|
$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)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
||||||
|
|
||||||
|
$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)
|
||||||
|
"@ | 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."
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Local development only. Generates a fresh OAuth 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.
|
||||||
|
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"
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
b64url() {
|
||||||
|
base64 | tr '+/' '-_' | tr -d '=\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
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"}
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENV_PATH="$ROOT_DIR/.env.development.local"
|
||||||
|
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
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Token written to $ENV_PATH (expires in ${EXPIRES_IN}s). Restart 'npm run dev' to pick it up."
|
||||||
Reference in New Issue
Block a user