feat(ops): emergency password-reset CLI + deployment docs
New CLI: bun run reset-password <email> Two modes selected interactively: - Recovery mode: if you still have the user's recovery code, unwrap the existing DEK with it and re-wrap with the new password. No data loss; the recovery code stays valid (mirrors /auth/recovery-complete). - Nuke mode: if both password AND recovery code are gone, generate a fresh DEK + new recovery code (printed once), and DELETE the user's private activities — their ciphertext is permanently unrecoverable. Public/semi/friends rows and engagement (hearts/bookmarks/done) are preserved. Both modes invalidate the user's sessions. Password length matches the signup/recovery rule (12 chars min). Wrong-recovery-code path aborts before any DB writes. Hand-rolled line reader sidesteps a Bun quirk where node:readline only delivers the first answer when stdin is piped. Also expand README's "Deployment" section: container snippet stays, plus new subsections for env vars, TLS termination (with a Caddyfile example), backup/restore via sqlite3 .backup, the /api/health healthcheck, upgrade flow, and a walkthrough of the reset CLI.
This commit is contained in:
parent
fb193b4914
commit
ef02b3f585
3 changed files with 388 additions and 3 deletions
106
README.md
106
README.md
|
|
@ -101,7 +101,9 @@ The server serves the SPA from `frontend/dist` in production. All non-`/api/*`,
|
|||
non-`/assets/*` requests fall through to `index.html` so client-side routing
|
||||
still works.
|
||||
|
||||
## Container (podman)
|
||||
## Deployment
|
||||
|
||||
### Container (podman)
|
||||
|
||||
The provided `Containerfile` builds a single image that serves API + frontend
|
||||
and persists the SQLite database in `/app/data` (one volume).
|
||||
|
|
@ -124,7 +126,107 @@ podman run --replace --name vinterliste \
|
|||
```
|
||||
|
||||
The container exposes `/api/health` for healthchecks and bakes the build date /
|
||||
git revision into both OCI labels and `/etc/build-info`.
|
||||
git revision into both OCI labels and `/etc/build-info`. Use `podman run
|
||||
--replace ...` for redeploys — it's atomic and avoids the "container exists"
|
||||
race.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|--------------------|------------------------|---------------------------------------------------------------|
|
||||
| `PORT` | `3000` | TCP port the server listens on. |
|
||||
| `NODE_ENV` | (unset) | Set to `production` to serve `frontend/dist` from the API. |
|
||||
| `VINTERLISTE_DB` | `data/vinterliste.db` | Path to the SQLite file. Override for an external volume. |
|
||||
| `PUBLIC_BASE_URL` | (derived from request) | Override the absolute URL used in OpenGraph `og:url` tags. |
|
||||
|
||||
There are no secrets to set. Auth verifiers and DEK wraps live in the SQLite
|
||||
file; session tokens are generated per process and stored server-side, not
|
||||
signed.
|
||||
|
||||
### TLS termination
|
||||
|
||||
The app speaks plain HTTP — terminate TLS at a reverse proxy (Caddy, nginx,
|
||||
Traefik). The session cookie is marked `Secure` when the request was HTTPS
|
||||
(`X-Forwarded-Proto: https`), so make sure the proxy sets that header.
|
||||
|
||||
Sample Caddyfile:
|
||||
|
||||
```caddyfile
|
||||
vinterliste.example.org {
|
||||
encode zstd gzip
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
```
|
||||
|
||||
Caddy auto-provisions a Let's Encrypt cert. Other proxies need the cert
|
||||
configured manually.
|
||||
|
||||
### Backup and restore
|
||||
|
||||
The SQLite database is the entire app state — user accounts, DEK wraps,
|
||||
activity ciphertexts, sessions, the lot. Backing it up while the server is
|
||||
running is safe because of WAL mode:
|
||||
|
||||
```bash
|
||||
# Atomic backup using SQLite's built-in copy
|
||||
sqlite3 data/vinterliste.db ".backup '/path/to/backup/vinterliste-$(date +%F).db'"
|
||||
|
||||
# Or via the container's volume
|
||||
podman exec vinterliste sqlite3 /app/data/vinterliste.db \
|
||||
".backup '/app/data/backup-$(date +%F).db'"
|
||||
```
|
||||
|
||||
Plain file copy of the `.db` works too if the server is stopped first. With WAL
|
||||
files (`.db-wal`, `.db-shm`) present, copy all three or use `.backup`.
|
||||
|
||||
To restore: replace the file on disk and restart the server. There are no
|
||||
out-of-band caches.
|
||||
|
||||
### Healthcheck
|
||||
|
||||
`GET /api/health` returns `{ ok: true, build: { revision, built_at } }` with
|
||||
HTTP 200. Hook your monitoring or `HEALTHCHECK` directive at this endpoint.
|
||||
|
||||
### Upgrading
|
||||
|
||||
1. Build a new image with current `BUILD_DATE` and `GIT_REVISION` args.
|
||||
2. `podman run --replace` — schema migrations are idempotent
|
||||
(`CREATE TABLE IF NOT EXISTS …` and `ensureColumn(...)` add new columns
|
||||
without touching existing data).
|
||||
3. Verify `/api/health` returns the new `revision`.
|
||||
4. The `activities` table's CHECK constraint includes all visibility values;
|
||||
the `friends` visibility added later is migrated in via
|
||||
`ensureActivitiesCheckIncludesFriends()` (table copy-drop-rename) on
|
||||
first boot if needed. Take a backup beforehand the first time you upgrade
|
||||
past a CHECK-constraint change.
|
||||
|
||||
### Emergency password reset (CLI)
|
||||
|
||||
If an admin has lost access (forgotten password, lost recovery code, etc.) and
|
||||
can't recover via the UI, the server box has a CLI tool:
|
||||
|
||||
```bash
|
||||
# Inside the container:
|
||||
podman exec -it vinterliste bun run reset-password admin@example.org
|
||||
|
||||
# Or on the host if you're running the server directly:
|
||||
bun run reset-password admin@example.org
|
||||
```
|
||||
|
||||
It asks one question first: **do you still have this user's recovery code?**
|
||||
|
||||
- **Yes → recovery mode.** Behaves exactly like the in-app recovery flow:
|
||||
unwraps the existing DEK with the recovery code, re-wraps it with the new
|
||||
password. No data is lost. The recovery code stays valid afterwards.
|
||||
- **No → nuke mode.** Generates a brand-new DEK + new recovery code and
|
||||
prints the new code to stdout (write it down — it's shown once). The
|
||||
user's **private activities are deleted** because their ciphertext was
|
||||
encrypted with the now-unrecoverable old DEK. Public, semi, friends-only
|
||||
activities, plus hearts / bookmarks / "gjort" marks, are kept.
|
||||
|
||||
Both modes invalidate every existing session for the user, matching the
|
||||
hygiene of the in-app `/auth/recovery-complete` endpoint. The CLI requires
|
||||
direct DB access — there is no network exposure of this code path.
|
||||
|
||||
## Registration: open, invite-only, or both
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue