From f4c8f8f21c265d24b55dc5204dc7b71a44edaa10 Mon Sep 17 00:00:00 2001 From: Steven RYDELL Date: Sat, 1 Aug 2026 18:05:43 +0200 Subject: [PATCH] feat(dev): add one-command local Stalwart test server + workflow docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 5 +- AGENTS.md | 6 ++- DEVELOPMENT.md | 108 ++++++++++++++++++++++++++++++++++++++++++ README.md | 6 ++- docker-compose.yml | 30 ++++++++++++ package.json | 3 ++ scripts/dev-token.ps1 | 60 +++++++++++++++++++++++ scripts/dev-token.sh | 63 ++++++++++++++++++++++++ 8 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 DEVELOPMENT.md create mode 100644 docker-compose.yml create mode 100644 scripts/dev-token.ps1 create mode 100644 scripts/dev-token.sh diff --git a/.gitignore b/.gitignore index e9a1b13..c45c992 100644 --- a/.gitignore +++ b/.gitignore @@ -23,13 +23,16 @@ dist-ssr *.sln *.sw? .ignore -scripts/ +scripts/* +!scripts/dev-token.ps1 +!scripts/dev-token.sh *.md !README.md !CHANGELOG.md !AGENTS.md !CLAUDE.md !SCHEMA_DEVIATIONS.md +!DEVELOPMENT.md !.agents/rules/*.md /SPEC-* # Tool-generated artifacts during agent sessions diff --git a/AGENTS.md b/AGENTS.md index 33ce16b..374ab99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,12 +8,16 @@ Community fork of [stalwartlabs/webui](https://github.com/stalwartlabs/webui), a ## 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 lint` - ESLint - `npm run test` - Vitest (`npm run test:watch` to watch) - `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 The detailed rules live in `.agents/rules/`. Read the relevant file before acting: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..bb52aba --- /dev/null +++ b/DEVELOPMENT.md @@ -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. diff --git a/README.md b/README.md index 9e4b32c..7137233 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Every tagged release of this fork publishes a `webui.zip` build via CI (see [`.g Prerequisites: - 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: @@ -146,12 +146,14 @@ VITE_OAUTH_SCOPES= ### 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 ``` +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 ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..12c34b3 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/package.json b/package.json index 5ae14f0..17e6912 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "type": "module", "scripts": { "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", "typecheck": "tsc -p tsconfig.app.json --noEmit", "lint": "eslint .", diff --git a/scripts/dev-token.ps1 b/scripts/dev-token.ps1 new file mode 100644 index 0000000..96b8cdd --- /dev/null +++ b/scripts/dev-token.ps1 @@ -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." diff --git a/scripts/dev-token.sh b/scripts/dev-token.sh new file mode 100644 index 0000000..b1baef8 --- /dev/null +++ b/scripts/dev-token.sh @@ -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 <&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" <