v0.15.0 password recovery, phone layout, CLI pager, embed policy fixes
test / test (push) Canceled after 0s
test / e2e (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-19 18:15:31 -04:00
1 parent a999177f0c
commit e1fa5242e3
48 files changed
+2101 -135

No files matched your search

+47
View File
@@ -116,6 +116,48 @@ Errors: 400 `INVALID_REQUEST` without a password, 401 for a wrong password or
token, 429 past 5 attempts a minute per IP, 503 `RESOURCE_LIMIT` when password
hashing is saturated or `STORAGE_UNAVAILABLE` when storage is down.
### Request a Password Reset
`POST /auth/recover`
Emails a one-time reset link to the account's address. The response is the
same whether or not the account exists or has an email, so nothing is
revealed.
**Request:**
```json
{"identifier": "alice"}
```
- `identifier` (string, required): Username or email address
**Response (202):**
```json
{"message": "If the account has an email address, a reset link has been sent to it."}
```
Errors: 429 past 3 requests a minute per IP, 503 `NOT_CONFIGURED` when the
server has no mail transport (`/health` reports `"mail": "disabled"`), 502
when the relay refused the message.
### Reset a Password
`POST /auth/reset`
Sets a new password with the token from the link (`?reset=<token>` on the web
client) and signs the user in; the token works once, for an hour, and every
other session of the account ends.
**Request:**
```json
{"token": "…", "password": "NewPass123"}
```
**Response (200):** as login (`token`, `userId`, `username`, `email`,
`expiresAt`).
Errors: 400 `INVALID_REQUEST` for a weak password or an invalid, used or
expired token (`"invalid or expired reset link"`), 429 past 10 a minute per
IP, 503 when storage or password hashing is unavailable.
## Game Endpoints
### Health Check
@@ -129,10 +171,14 @@ Returns server and storage status, and the build the server runs.
"status": "healthy",
"time": 1699123456,
"storage": "ok",
"mail": "disabled",
"version": "v0.14.0-3-g1a2b3c4"
}
```
`mail` is `"ok"` when the server can email password-reset links (the web
client then offers **Forgot password?**), otherwise `"disabled"`.
`version` is `git describe` of the source the binary was built from (`make`
sets it), or `dev` for a plain `go build`. Compare it after a deployment: a
client newer than the server gets 404 `NOT_FOUND` for endpoints the server
@@ -556,6 +602,7 @@ Error codes:
- Registration: 5 requests/minute/IP
- Login: 10 requests/minute/IP
- Account deletion: 5 requests/minute/IP
- Password recovery: 3 requests/minute/IP; reset: 10 requests/minute/IP
Exceeding limit returns 429 status. Behind a reverse proxy the client IP is the
proxy's `X-Real-IP` header, trusted only from addresses listed in
+7
View File
@@ -294,6 +294,13 @@ Exit the client.
chess > exit
```
### Long output
`help`, `games`, `pgn`, `state`, `show` and `replay` print one screen at a
time when their output is taller than the terminal: `-- More (N lines left;
Enter continues, q quits) --`. The terminal's size comes from the TTY, or in
the browser from xterm.js; output piped to another program is never paged.
### Verbose Mode
Append `-v` flag to any command for detailed output:
+9 -3
View File
@@ -131,9 +131,15 @@ sudo chess-db user list
su -m chess -c '/home/chess/bin/chess-server db user add -username <name> -dsn "postgres:///chess?host=/tmp"'
```
Other `user` subcommands: `delete`, `set-email`, `set-username`, and
`set-hash` (an existing PHC string). CLI-created accounts are identical to
site registrations, and the CLI ignores the registration cap.
Other `user` subcommands: `delete`, `set-email`, `set-username`, `set-hash`
(an existing PHC string), and `reset-link -url <web client>`, which prints a
one-time password-reset link (valid an hour) to hand to someone who cannot
receive the recovery email. CLI-created accounts are identical to site
registrations, and the CLI ignores the registration cap.
Pending password resets live in `password_resets`: the SHA-256 of the token
in the emailed link, one per account, deleted when used, expired, or by the
hourly cleanup. The token itself is never stored.
Sign a user out everywhere (takes effect on their next request):
+1 -1
View File
@@ -120,7 +120,7 @@ file is kept next to it with a timestamp).
| `CHESSD_DSN` | `"host=<socket dir> dbname=chess"` | Database over the socket as the OS account |
| `CHESSD_JWT_KEY` | `/var/lib/chessd/jwt.key` | Stable signing key |
| `CHESSD_TRUSTED_PROXIES` | `TRUSTED_PROXIES` | Comma-separated proxy addresses |
| `CHESSD_FLAGS` | empty | Other server flags, e.g. `-max-users 500 -db-cleanup report` |
| `CHESSD_FLAGS` | empty | Other server flags, e.g. `-max-users 500 -db-cleanup report`, or password recovery by email: `-smtp-host <relay> -mail-from "Chess <chess@example.com>" -public-url https://example.com/chess/web-client/` (see [deployment.md](deployment.md#password-recovery-by-email); the sandboxed unit can read a password file only under `/var/lib/chessd`) |
Server flags worth knowing: `-max-users` (registration cap, default 100),
`-anonymous-game-ttl` (default `24h`), `-finished-game-ttl` (default `1h`),
+50 -1
View File
@@ -154,6 +154,10 @@ as `postgres`, list them with `\dt chess.*` and name them as `chess.games`.
| `chessd_dsn` | `postgres:///chess?host=/tmp` | Database `chess` over the socket as the OS user |
| `chessd_jwt_key` | `${chessd_home}/jwt.key` | JWT signing key; created (0600) on first start if missing |
| `chessd_trusted_proxies` | empty | Proxy address(es) whose `X-Real-IP` is trusted |
| `chessd_public_url` | empty | The web client's public address, which emailed reset links open (`https://<site>/chess/web-client/`) |
| `chessd_smtp_host`, `chessd_smtp_port` | empty, `25` | SMTP relay for password-reset email; unset leaves recovery off |
| `chessd_smtp_user`, `chessd_smtp_password_file` | empty, `${chessd_home}/smtp.secret` if present | Relay credentials, when the relay authenticates this host; the file is mode 0600 |
| `chessd_mail_from` | empty | Sender address, e.g. `Chess <chess@<site>>` |
| `chessd_flags` | empty | Extra flags, e.g. `-max-users 500 -db-cleanup report` |
The script runs `daemon(8)` as root, which writes the supervisor pidfile
@@ -174,6 +178,11 @@ Server flags:
| `-anonymous-game-ttl` | | Retention of games without a registered player (default `24h`) |
| `-finished-game-ttl` | | Memory retention of finished games (default `1h`) |
| `-db-cleanup` | | Hourly integrity sweep: `off` (default), `report` (log findings), or `delete` ([database.md](database.md#integrity-sweep)) |
| `-smtp-host`, `-smtp-port` | | SMTP relay for password-reset email (default port 25); recovery is off without a transport |
| `-smtp-user`, `-smtp-password-file` | | Relay credentials, optional; the file is mode 0600 and only sent over TLS |
| `-mail-from` | | Sender address of server email |
| `-mail-dir` | | Development: write email to `.eml` files here instead of a relay |
| `-public-url` | | The web client's address that emailed links open; required with a transport |
Administrative CLI commands run as `chess` so peer authentication selects the
right role; root may use `su -m` although the account has no shell:
@@ -186,7 +195,47 @@ jail# su -m chess -c '/home/chess/bin/chess-server db user list -dsn "postgres:/
(games, with 8-digit ID prefixes), `pgn -gameId <id|prefix> [-ply N]`,
`verify [-gameId <id|prefix>]` (replays stored games against the rules and
exits non-zero on a mismatch; run it after an upgrade), and `user add|delete|
set-password|set-hash|set-email|set-username|list`.
set-password|set-hash|set-email|set-username|reset-link|list`.
A forgotten password is reset from the jail without mail:
```sh
jail# su -m chess -c '/home/chess/bin/chess-server db user set-password -username <name> -dsn "postgres:///chess?host=/tmp"'
```
which prompts for the new password (or takes `-password`), or with a one-time
link to hand to the person, the same link the recovery email carries:
```sh
jail# su -m chess -c '/home/chess/bin/chess-server db user reset-link -username <name> -url https://<site>/chess/web-client/ -dsn "postgres:///chess?host=/tmp"'
```
## Password Recovery by Email
With an SMTP relay the server emails a reset link on request (`POST
/api/auth/recover`, the **Forgot password?** link in the web client); the link
opens the client, which sets the new password and signs the person in. The
link works for an hour and once; using it signs the account out everywhere
else. Accounts without an email address cannot use it (the CLI commands above
can).
The relay is a separate jail reached over the internal network. Set in
`rc.conf`:
```sh
sysrc chessd_public_url="https://<site>/chess/web-client/"
sysrc chessd_smtp_host="<mail jail address>" # chessd_smtp_port, default 25
sysrc chessd_mail_from="Chess <chess@<site>>"
# only when the relay authenticates this host:
sysrc chessd_smtp_user="<user>"
(umask 077 && echo '<password>' > /home/chess/smtp.secret) && chown chess:chess /home/chess/smtp.secret
service chessd restart
```
The server uses STARTTLS when the relay offers it and sends credentials only
over TLS. `GET /chess/health` reports `"mail": "ok"` once recovery is on. For
a test without a relay, `-mail-dir <dir>` in `chessd_flags` writes each
message to a file instead.
## Host nginx and Web Clients
+6 -3
View File
@@ -38,6 +38,9 @@ go build ./cmd/chess-client-cli
- `-anonymous-game-ttl`: How long games without a registered player survive after their last activity, in memory and in the database (default: `24h`; `0` keeps them)
- `-max-users`: Accounts at which public registration closes (default: `100`; `0` = no limit; the CLI is not limited)
- `-web-api-url`: Browser-visible API origin for the embedded web client, set on its script element as `data-api`; useful when its public origin differs from the listen address
- `-smtp-host`, `-smtp-port`, `-smtp-user`, `-smtp-password-file`, `-mail-from`: SMTP relay for password-reset email; without a transport, recovery is off
- `-mail-dir`: Development: write email to `.eml` files in this directory instead of sending
- `-public-url`: The web client's address that emailed links open (required with a transport)
### Modes
```bash
@@ -223,8 +226,8 @@ go test ./...
go test -race ./internal/server/storage ./internal/server/service
# Web client end-to-end (Playwright): see test/README.md
WEB_PORT=9090 test/run-test-server.sh # terminal 1
make test-e2e # terminal 2
WEB_PORT=9090 MAIL_DIR=/tmp/chess-mail test/run-test-server.sh # terminal 1
CHESS_E2E_MAIL_DIR=/tmp/chess-mail make test-e2e # terminal 2
```
## Configuration
@@ -308,7 +311,7 @@ make test-e2e # terminal 2
- JWT tokens don't support refresh (must re-login after expiry)
- User deletion doesn't cascade to games (games keep player and claim IDs)
- Games played without a registered player are deleted 24 hours after their last activity
- No password recovery mechanism
- Password recovery needs an SMTP relay and an email on the account
- No email verification for registration
- Fixed worker pool size for engine calculations
- No push-based game updates (30-second long-polling is used)
+24 -1
View File
@@ -174,13 +174,36 @@ Test gate:
while replaying, `show` shows the replayed position, and the prompt reads
`replay <ply>/<plies>`; `live` returns to the game as it was.
Follow-up polish (done): the phone layout is full-width rows under the board
(`svh` units, so the browser's bars never cover the controls; a replay
reserves more height than live play), the turn strip says whose move it is in
two words with the dot showing the side, the sign-in dialog keeps the focus in
the password field after a wrong password, the brand mark is the site's
bishop, and the terminal client pages long output (help, games, pgn, state,
show, replay) one screen at a time, natively and in the browser.
Later for the replay clients:
- [ ] Web: an "Analysis" link on a replay (chess.com-style evaluation needs
an engine in the browser or a server endpoint; none exists).
- [ ] Web: a light theme, following the host page's choice.
- [ ] CLI: `auto` cannot be interrupted before the end of the line.
## Accounts
- [x] Password recovery by email: `POST /api/auth/recover` sends a one-time
link (an hour, SHA-256 of the token stored in `password_resets`, schema
v4) to the account's address through an SMTP relay (`-smtp-host`, with
optional credentials over TLS) or, for development, a directory of `.eml`
files (`-mail-dir`); `POST /api/auth/reset` sets the password, ends the
account's other sessions and signs the person in; the web client offers
**Forgot password?** when `/health` reports `"mail": "ok"` and opens the
password form from `?reset=<token>`. `db user reset-link` prints the same
link for an administrator; `db user set-password` resets without one.
- [ ] Deliver from the chess jail through the mail jail (devops): relay
address, sender, and `chessd_smtp_*` in `rc.conf` (deployment.md).
- [ ] Email verification at registration and on `set-email`; today an
address is taken as typed, so a typo sends nobody a link.
## Phase R5 — Curated Archive (later)
- [ ] Schema: game origin (`player`, `curated`), archive metadata (event, site,