fix(backend): JWT jti, NUL-byte guard, dev-only truncate endpoint

Two bugs surfaced while running the new E2E suite, plus a small test hook:

- jwt.rs: add a per-token `jti: Uuid` claim. Without it, two `create_token`
  calls in the same wall-clock second for the same (sub, role, event_id)
  produced identical JWT bytes — and identical sha256(token) hashes —
  which then collided on `session.token_hash UNIQUE` with a 500. Manifests
  in real use when an admin clicks "Anmelden" twice fast.

- auth/handlers.rs: reject display names containing 0x00. Postgres rejects
  NUL in TEXT columns with `invalid byte sequence for encoding "UTF8"` and
  the request leaks back as a 500. Now returns 400 with a clean message.

- handlers/test_admin.rs + main.rs: new POST /api/v1/admin/__truncate route,
  compiled in always but only **registered** when EVENTSNAP_TEST_MODE=1 is
  set on startup. Truncates every event-scoped table, reseeds config from
  migration defaults, wipes media on disk, and clears the in-memory rate
  limiter. RequireAdmin-gated so it's not anonymous even in test mode. In
  production builds (no env var) the route returns 404 — verified by the
  startup log message.

- services/rate_limiter.rs: add `clear()` so the truncate handler can wipe
  the in-memory window map between tests.

- Dockerfile: bump rust:1.87 → rust:1.88 (current dep tree needs it) and
  COPY ./migrations into the build context so the `sqlx::migrate!()` macro
  can resolve at compile time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-16 19:01:34 +02:00
parent e6efffafe5
commit 05f76514a2
7 changed files with 126 additions and 1 deletions

View File

@@ -107,6 +107,23 @@ async fn main() -> Result<()> {
)
.route("/api/v1/admin/export/jobs", get(handlers::admin::get_export_jobs));
// Test-only route: a hard reset for the Playwright E2E harness. The handler
// is compiled in always, but the route is only attached when
// `EVENTSNAP_TEST_MODE=1`. In production the call returns 404 — the route
// simply isn't there.
let api = if handlers::test_admin::is_test_mode() {
tracing::warn!(
"EVENTSNAP_TEST_MODE=1 — registering /api/v1/admin/__truncate. \
DO NOT enable this in production."
);
api.route(
"/api/v1/admin/__truncate",
post(handlers::test_admin::truncate_all),
)
} else {
api
};
// Serve media files from disk
let media_service = ServeDir::new(&config.media_path);