L10b Betrieb & Aufräumen: /api/v1 über gemeinsamen Adapter, einheitliches Fehlerformat, Rate Limiting

Aufräumpunkt a: Die lane-lokalen API-Kontexte (imports/_context.ts, sync/api-context.ts,
reports/http.ts, work-orders/_http.ts mit moduleGuard) sind entfernt. Alle v1-Routen laufen über
requireApiContext (DB-autoritative Rechte, 401/403) und withApi/toErrorResponse (respond.ts):
- Fehlerformat überall { error: { code, message, details? } }; invalid und blocked → 422,
  conflict → 409, payload_too_large → 413, rate_limited → 429 + Retry-After.
- Same-Origin-Prüfung in withApi für jede Mutation vor der Anmeldung (vorher fehlte sie bei
  imports, reports und work-orders).
- Rate Limiting je Nutzer mit rate-limit.ts: api (API_RATE_LIMIT_PER_MINUTE, 300/min) und
  apiField für sync/uploads/field (API_FIELD_RATE_LIMIT_PER_MINUTE, 1200/min).
- Clients angepasst: Import-Uploader liest das neue Fehlerformat, Upload/Outbox werten 422 als
  endgültig ungültig (429 bleibt transient mit Backoff).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:19:19 +02:00
co-authored by Claude Opus 5
parent a7d4b02a13
commit fb993a7730
29 changed files with 333 additions and 483 deletions
+9 -8
View File
@@ -1,11 +1,12 @@
import { requireApiContext } from "@/server/api/context";
import { json, withApi } from "@/server/api/respond";
import { approveReport } from "@/server/services/reports/approve";
import { reportDto, withReportsApi } from "@/server/services/reports/http";
import { reportDto } from "@/server/services/reports/dto";
/** POST /api/v1/reports/:id/approve — team lead → team_approved, backoffice → approved (+ PDF job). */
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
/** POST /api/v1/reports/:id/approve — team lead → team_approved, backoffice → approved (+ PDF job). Approval rights are checked in the service. */
export const POST = withApi(async (_req: Request, { params }: { params: Promise<{ id: string }> }) => {
const ctx = await requireApiContext("reports", "report:read");
const { id } = await params;
return withReportsApi(["report:read"], async (ctx) => {
const report = await approveReport(ctx, { reportId: id });
return Response.json({ report: reportDto(report) });
});
}
const report = await approveReport(ctx, { reportId: id });
return json({ report: reportDto(report) });
});
@@ -1,9 +1,11 @@
import { requireApiContext } from "@/server/api/context";
import { withApi } from "@/server/api/respond";
import { fileResponse, openReportFile } from "@/server/services/reports/files";
import { withReportsApi } from "@/server/services/reports/http";
/** GET /api/v1/reports/:id/files/:documentId — photo/signature/logo referenced by the report snapshot. */
export async function GET(req: Request, { params }: { params: Promise<{ id: string; documentId: string }> }) {
export const GET = withApi(async (req: Request, { params }: { params: Promise<{ id: string; documentId: string }> }) => {
const ctx = await requireApiContext("reports", "report:read");
const { id, documentId } = await params;
const download = new URL(req.url).searchParams.get("download") === "1";
return withReportsApi(["report:read"], async (ctx) => fileResponse(await openReportFile(ctx, id, documentId), { download }));
}
return fileResponse(await openReportFile(ctx, id, documentId), { download });
});
+6 -4
View File
@@ -1,9 +1,11 @@
import { requireApiContext } from "@/server/api/context";
import { withApi } from "@/server/api/respond";
import { fileResponse, openReportFile } from "@/server/services/reports/files";
import { withReportsApi } from "@/server/services/reports/http";
/** GET /api/v1/reports/:id/pdf — the immutable PDF of an approved report (?download=1 for attachment). */
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
export const GET = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
const ctx = await requireApiContext("reports", "report:read");
const { id } = await params;
const download = new URL(req.url).searchParams.get("download") === "1";
return withReportsApi(["report:read"], async (ctx) => fileResponse(await openReportFile(ctx, id, "pdf"), { download }));
}
return fileResponse(await openReportFile(ctx, id, "pdf"), { download });
});