feat(timetable): 60s Redis idempotency-key middleware

RFC v0.2 R1 — Idempotency-Key replay window for POST
/api/v1/events/{event}/timetable/move. Narrow scope by design: the
12-hour ARCH §10 default would let a cached cascade-bump response
overwrite a fresh edit; 60 seconds covers honest network retry but
expires before a meaningful conflict can emerge.

Backed by the Laravel Cache facade (Redis in non-test env). Cache key
namespace `idempotency:60s:*` distinct from FormSubmission's
DB-column idempotency. Replays carry an `Idempotency-Replayed: true`
header so observability can distinguish them.

Registered as the route-middleware alias `idempotency.60s` in
bootstrap/app.php; will be applied on the move route in Step 8.

Missing or empty Idempotency-Key returns 400 with
`{"error":"idempotency_key_required"}`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 20:54:20 +02:00
parent 9e94ab78d8
commit 546f121ee8
2 changed files with 79 additions and 0 deletions

View File

@@ -59,6 +59,9 @@ return Application::configure(basePath: dirname(__DIR__))
'portal.token' => \App\Http\Middleware\PortalTokenMiddleware::class,
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'impersonation' => \App\Http\Middleware\HandleImpersonation::class,
// RFC-TIMETABLE v0.2 R1 — 60s Redis idempotency window for
// POST /timetable/move. Narrow scope by design.
'idempotency.60s' => \App\Http\Middleware\IdempotencyKey60sRedis::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {