Files
Mangalord/README.md
MechaCat02 57364fae32 chore: release-prep docs, env vars, compose, and e2e port hygiene
- README rewritten end-to-end: stack, quick start, dev workflow, full
  /api/v1 endpoint table, error and pagination envelopes, auth
  quick-start (browser + bot bearer), configuration table, deployment
  notes, backup/restore pointer. Stale "next features" section dropped
  now that all eight feat branches are in.
- .env.example now lists every env var the backend reads, with
  inline explanations:
  - COOKIE_SECURE / COOKIE_DOMAIN / SESSION_TTL_DAYS (auth)
  - CORS_ALLOWED_ORIGINS (same-origin by default)
  - MAX_REQUEST_BYTES / MAX_FILE_BYTES (upload caps)
  - Postgres + storage + log vars carried over.
- docker-compose.yml forwards all of the above into the backend
  service with `${VAR:-default}` so an unset value falls back to the
  same default the code uses, and any `.env` override flows through
  without a compose edit.
- docs/backup.md: step-by-step backup, restore, and smoke-test drill
  for both stateful volumes (postgres-data + storage-data), plus a
  list of what's deliberately *not* in the backup (e.g., .env).
- playwright.config.ts: pins the e2e dev server to port 5174 with
  `--strictPort` so it neither reuses nor silently bumps off
  collision with another vite instance on 5173. Drops the flaky
  manual-start workflow the earlier branches needed.
- docker-compose syntax (both prod and dev) validates cleanly against
  .env.example with no undefined-variable warnings.

No version bump — this is documentation, config, and tooling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:58:49 +02:00

210 lines
10 KiB
Markdown

# Mangalord
A self-hosted manga and comics reader. Users browse, search, read, bookmark, and upload manga. The same HTTP API is consumed by the SvelteKit web UI and by external bots — there is no separate "bot API" — so anything you can do in the browser you can also do over `curl` with a bearer token.
## Stack
- **Backend**: Rust, axum 0.7, sqlx 0.8, Postgres 16. Argon2id passwords, HttpOnly SameSite=Lax session cookies, bearer tokens for bots.
- **Frontend**: SvelteKit 2 with Svelte 5 runes, TypeScript.
- **Storage**: pluggable `Storage` trait. `LocalStorage` ships in-tree; S3 and friends are first-class extension slots.
- **Deploy**: a single host via Docker Compose.
## Quick start
```bash
cp .env.example .env
docker compose up --build
```
| Service | URL |
| ----------------- | ---------------------------------- |
| Frontend | <http://localhost:3000> |
| API base | <http://localhost:8080/api/v1> |
| Health check | <http://localhost:8080/api/v1/health> |
The first boot runs the migrations automatically. From there:
1. Open <http://localhost:3000>, click **Register**, create a user.
2. Click **Upload** to create a manga (optionally with a cover) and upload a chapter.
3. Open the manga, click the chapter, read with `j`/`k` or the arrow keys.
## Local development
For fast iteration, run Postgres in Docker and the backend + frontend natively:
```bash
docker compose -f docker-compose.dev.yml up -d
# Backend (separate shell)
cd backend
export DATABASE_URL=postgres://mangalord:mangalord@localhost:5432/mangalord
cargo run
# Frontend (separate shell)
cd frontend
npm install
npm run dev # serves on http://localhost:5173, proxies /api to :8080
```
> **Port note:** `docker-compose.dev.yml` exposes Postgres on `5432`. If another Postgres is already bound to that port (or another Docker container is), stop it first or edit the host-port mapping in the dev compose file. Backend tests need a real Postgres that the test user can `CREATEDB` on; the dev container works because the `mangalord` role is the cluster superuser.
## Tests
This project is developed test-first. Three levels:
```bash
# Backend: unit (in-module) + integration (#[sqlx::test] against a real DB)
cd backend && cargo test
cd backend && cargo clippy --all-targets -- -D warnings
# Frontend: Vitest unit tests and svelte-check
cd frontend && npm test
cd frontend && npm run check
# End-to-end: Playwright (mocks the backend at the page level by default)
cd frontend && npm run test:e2e
```
Backend integration tests provision a fresh, migrated database per test via `sqlx::test`, so they require `DATABASE_URL` to point at a Postgres where the configured user has `CREATEDB`.
Playwright starts the SvelteKit dev server on a dedicated port (`5174`) so it doesn't reuse whatever you may have running on `5173`. To point Playwright at an already-running deployment instead, set `E2E_BASE_URL=http://your.host`.
## API surface
Everything is namespaced under `/api/v1/`. `/api/*` outside the version prefix is reserved for future versioning.
### Read endpoints (public)
| Method | Path | Description |
| ------ | ------------------------------------------------------- | ------------------------------------------ |
| GET | `/api/v1/health` | Liveness probe. |
| GET | `/api/v1/mangas?search=&sort=recent|title&limit=&offset=` | List/search mangas. Trigram fuzzy search. |
| GET | `/api/v1/mangas/{id}` | Single manga. |
| GET | `/api/v1/mangas/{id}/chapters` | Paginated chapter list, ordered by number. |
| GET | `/api/v1/mangas/{id}/chapters/{n}` | Single chapter. |
| GET | `/api/v1/mangas/{id}/chapters/{n}/pages` | Page metadata (storage keys + MIME). |
| GET | `/api/v1/files/{key}` | Streams a blob (cover or page). |
### Write endpoints (require authentication)
| Method | Path | Description |
| ------ | ------------------------------------------------------- | ------------------------------------------------------ |
| POST | `/api/v1/auth/register` | `{username,password}` → user + sets session cookie. |
| POST | `/api/v1/auth/login` | Same shape; rotates to a fresh session. |
| POST | `/api/v1/auth/logout` | Invalidates the session, clears the cookie. |
| GET | `/api/v1/auth/me` | Current user, or 401 if anonymous. |
| POST | `/api/v1/auth/tokens` | `{name}` → bot bearer token (returned once). |
| DELETE | `/api/v1/auth/tokens/{id}` | Revoke a bot token (owner-only). |
| POST | `/api/v1/mangas` | Multipart: `metadata` JSON + optional `cover` image. |
| POST | `/api/v1/mangas/{id}/chapters` | Multipart: `metadata` JSON + N `page` image parts. |
| POST | `/api/v1/bookmarks` | `{manga_id, chapter_id?, page?}`. |
| DELETE | `/api/v1/bookmarks/{id}` | Remove a bookmark (owner-only). |
| GET | `/api/v1/me/bookmarks?limit=&offset=` | The caller's bookmarks. |
### Envelopes
**Errors** — every non-2xx response:
```json
{ "error": { "code": "snake_case", "message": "human readable", "details": { "field?": "..." } } }
```
`details` is only present on `validation_failed` (422). `code` is the stable contract; the message is for humans and may change.
| HTTP | code |
| ---- | -------------------------- |
| 400 | `invalid_input`, `bad_file_key` |
| 401 | `unauthenticated` |
| 403 | `forbidden` |
| 404 | `not_found` |
| 409 | `conflict` |
| 413 | `payload_too_large` |
| 415 | `unsupported_media_type` |
| 422 | `validation_failed` |
| 500 | `internal_error` |
**Lists** — every paginated endpoint:
```json
{
"items": [ /* ... */ ],
"page": { "limit": 50, "offset": 0, "total": 137 }
}
```
`total` is a number when the endpoint computes it (currently `/mangas`), otherwise `null`.
## Auth quick-start
### Browser
The frontend handles this for you: register → cookie set → writes work. Cookies are `HttpOnly; SameSite=Lax; Secure` (configurable via `COOKIE_SECURE`).
### Bots / scripts (bearer tokens)
```bash
# 1. Log in once via cookies (or register).
curl -sb -c cookies.txt -X POST http://localhost:8080/api/v1/auth/login \
-H 'content-type: application/json' \
-d '{"username":"alice","password":"hunter2hunter2"}'
# 2. Mint a long-lived bot token. The `bearer` value is shown ONCE.
curl -sb cookies.txt -X POST http://localhost:8080/api/v1/auth/tokens \
-H 'content-type: application/json' \
-d '{"name":"ci-bot"}'
# → { "id": "...", "name": "ci-bot", "bearer": "raw-token-here", ... }
# 3. Use the bearer from anywhere.
curl -H 'Authorization: Bearer raw-token-here' \
http://localhost:8080/api/v1/auth/me
```
Tokens are stored hashed (sha256) at rest; the raw value never leaves the response that created it. Revoke with `DELETE /api/v1/auth/tokens/{id}` while authenticated as the owner.
## Configuration
All variables can be set in `.env` (for `docker compose`) or your shell (for `cargo run`). See `.env.example` for the full list with explanations.
| Variable | Default | Purpose |
| ----------------------- | ------------------------ | --------------------------------------------- |
| `DATABASE_URL` | required | Postgres connection string. |
| `BIND_ADDRESS` | `0.0.0.0:8080` | Backend listen address. |
| `STORAGE_DIR` | `./data/storage` | Local-storage root. |
| `RUST_LOG` | `info,mangalord=debug` | tracing-subscriber env filter. |
| `COOKIE_SECURE` | `true` | `Secure` flag on the session cookie. |
| `COOKIE_DOMAIN` | (none) | Scope the session cookie to a parent domain. |
| `SESSION_TTL_DAYS` | `30` | Session lifetime. |
| `CORS_ALLOWED_ORIGINS` | (empty → same-origin) | Comma-separated origin allowlist. |
| `MAX_REQUEST_BYTES` | `209715200` (200 MiB) | Hard cap on multipart request size. |
| `MAX_FILE_BYTES` | `20971520` (20 MiB) | Cap on a single image part. |
| `PUBLIC_API_BASE` | `http://localhost:8080/api` | Browser-facing API base. |
## Deployment
For real hosts:
- **Front Mangalord with a TLS terminator** (Caddy, nginx, traefik). Point `:443` at the frontend on `:3000` and proxy `/api/*` to the backend on `:8080`. With same-origin routing you can leave `CORS_ALLOWED_ORIGINS` empty and the session cookie's `SameSite=Lax` does its job.
- **Set a strong Postgres password** in `.env` before the first `docker compose up`. The defaults are fine for local dev only.
- **Keep `COOKIE_SECURE=true`** behind HTTPS. Browsers drop `Secure` cookies on plain HTTP; the dev compose accepts `COOKIE_SECURE=false` for that case.
- **Watch `RUST_LOG`** if you're noisy with `debug` — drop the `,mangalord=debug` suffix in production to log at `info` for the app's spans.
## Backup and restore
Two stateful volumes:
- `postgres-data` — all metadata (users, mangas, chapters, pages, bookmarks).
- `storage-data` — the actual cover and page bytes.
A consistent backup needs **both**. See [docs/backup.md](docs/backup.md) for step-by-step procedures and a restore drill.
## Project layout
```
backend/ # axum + sqlx, see backend/src/ for the hexagonal layout
frontend/ # SvelteKit 2 / Svelte 5
docs/ # operator docs (backup procedure, etc.)
CLAUDE.md # contributor playbook
```
The contributor guidelines (TDD, Conventional Commits, lockstep SemVer, hexagonal seams) are in [CLAUDE.md](CLAUDE.md).