Files
craftvia/docs/craftvia/API.md
T
msolarczekandClaude Opus 5 49f05c8db3 L10b Betrieb & Aufräumen: OpenAPI 3.1 unter /api/v1/openapi.json, API-Doku und API-Test
- src/lib/api/openapi.ts: statisch gepflegte Spezifikation aller v1-Routen inkl. Fehlerformat,
  Pagination, Idempotenz (clientOpId/clientId), Konflikte, Rate Limits, Rechte je Operation.
- GET /api/v1/openapi.json liefert das Dokument (angemeldete Nutzer).
- docs/craftvia/API.md: Kurzdoku mit Endpunkt-Tabelle.
- scripts/test-betrieb-api.ts: jede Route nutzt requireApiContext/respond.ts, 401 ohne Sitzung
  im einheitlichen Format, 403 bei fremdem Origin/Sec-Fetch-Site, Fehler-Mapping, Rate Limit je
  Nutzer (Standard/Einsatz getrennt), OpenAPI deckt jede route.ts ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00

8.3 KiB
Raw Blame History

Craftvia API (/api/v1)

Versionierte JSON-API für Backoffice-Formulare, die Mobile-App/PWA (Offline-Sync) und künftige Integrationen. Die maschinenlesbare Spezifikation (OpenAPI 3.1) liefert GET /api/v1/openapi.json (gepflegt in src/lib/api/openapi.ts, API_ROUTES listet alle dokumentierten Pfade). Jede neue oder geänderte src/app/api/v1/**/route.ts muss dort nachgetragen werden.

Authentifizierung und CSRF

  • Session-Cookie von Auth.js: authjs.session-token (unter HTTPS __Secure-authjs.session-token). Ohne Cookie antwortet bereits der Proxy (src/proxy.ts) mit 401.
  • Rechte werden bei jedem Request aus der Datenbank gelesen (Mitgliedschaft, Identitätsstatus, Session-Kill-Switch, Passwortwechsel, effektive Rechte), nie aus dem JWT. Fehlt ein Recht oder ist das Modul des Mandanten deaktiviert, kommt 403.
  • Sichtbarkeit: Objekte eines fremden Mandanten oder außerhalb des eigenen Scopes (z. B. Monteur ↔ fremder Auftrag) liefern 404, nicht 403.
  • CSRF: Schreibende Methoden (POST/PATCH) nur Same-Origin: Der Origin-Header muss zum Host passen, Sec-Fetch-Site muss same-origin oder none sein. Sonst 403 forbidden.

Fehlerformat

Alle Routen antworten im Fehlerfall mit Cache-Control: no-store und

{ "error": { "code": "invalid", "message": "validation failed", "details": [{ "path": "customerId", "code": "too_small" }] } }
Code HTTP Bedeutung / details
unauthorized 401 nicht angemeldet, Konto inaktiv, Sitzung invalidiert
forbidden 403 Recht fehlt, Modul deaktiviert, Passwortwechsel nötig, Cross-Site-Request
not_found 404 unbekannt, fremder Mandant oder außerhalb des Scopes
conflict 409 Versionskonflikt (baseVersion), Doppelbestätigung/unzulässiger Zustand, mögliche Dubletten (details.reason = "possible_duplicates", details.candidates)
invalid 422 Validierung (Zod: details = [{ path, code }]), fehlerhaftes JSON/Multipart
blocked 422 fachlich gesperrt, z. B. details = CompletionBlocker[]
payload_too_large 413 Datei/Body zu groß
rate_limited 429 Header Retry-After (Sekunden), details.retryAfterSeconds
internal 500 unerwarteter Fehler, keine internen Details

Pagination

GET /customers, GET /sites und GET /sites/{id}/history verwenden ?page (≥ 1) und ?pageSize (1–100, Standard 25, bei der Historie 50). Antwort: { "data": [...], "pagination": { "page", "pageSize", "total" } } (Historie zusätzlich meta.onlyApproved). GET /work-orders hat ein eigenes Format: { items, total, page, pageSize, groupCounts } (groupCounts = Anzahl je Statusgruppe ohne Status-/Gruppenfilter).

Idempotenz und Konflikte (Sync)

  • POST /sync nimmt { deviceId, operations[] } mit 1–100 Operationen an (die PWA-Outbox schickt Batches ≤ 50). Jede Operation hat eine clientOpId (UUID) und wird einzeln angewendet. Die HTTP-Antwort ist 200, das Ergebnis steht je Operation in results[]: applied | duplicate | conflict | rejected (mit errorCode, message, idMap, entityVersion).
  • Idempotenz: Eine wiederholte clientOpId (je Mandant) liefert duplicate mit dem gespeicherten Ergebnis. Ist die ID bereits durch einen anderen Nutzer belegt, wird die Operation rejected.
  • Konflikte: work_order.transition und report.submit verlangen baseVersion. Weicht sie von WorkOrder.version ab, lautet das Ergebnis conflict, entityVersion ist dann die aktuelle Version. Alle anderen Operationen sind additiv (Client-IDs in den Payloads, z. B. clientId, werden über idMap auf Server-IDs abgebildet).
  • Den opType-Katalog mit den Payload-Schemas enthält src/lib/sync/ops.ts (Spec: Komponenten SyncPayload*).
  • REST-Schreibrouten für Aufträge (PATCH /work-orders/{id}, /assign, /transition) akzeptieren optional baseVersion und antworten bei Abweichung mit 409.

Uploads

  • POST /uploads (Einsatz): multipart mit file, clientId (UUID), workOrderId, kind (photo | voice_note) und optional preview (Thumbnail ≤ 2 MB). Maximal 25 MB, der Inhalt wird per Magic Bytes geprüft. Idempotent über clientId: dieselbe clientId liefert 200 { documentId, duplicate: true }, ein neuer Upload 201 { documentId, duplicate: false }. Die documentId wird danach in photo.attach/voice.attach referenziert.
  • POST /work-orders/{id}/documents: multipart mit file, category, visibility, title?. Antwort 201. Mit Accept: text/html kommt stattdessen ein 303-Redirect (Backoffice-Formular).
  • POST /work-orders/import: multipart mit file (PDF/JPEG/PNG, ≤ 25 MB), Antwort 201 { id, status }. Die Extraktion läuft asynchron.

Rate Limits

Die Zählung erfolgt je Nutzer in einem Fenster von einer Minute, im Speicher je App-Instanz (bei mehreren Instanzen also pro Instanz).

  • Standard: API_RATE_LIMIT_PER_MINUTE (Default 300)
  • Einsatz-Endpunkte /sync, /uploads, /field/**: API_FIELD_RATE_LIMIT_PER_MINUTE (Default 1200)

Bei Überschreitung kommt 429 mit Retry-After.

Endpunkte

Die Pfade sind relativ zu /api/v1. „Recht“ nennt das Gate der Route. Mit „Service“ markierte Rechte prüft der Service (zusätzlich zum Scope).

Methode Pfad Modul Recht Beschreibung
GET /customers customers customer:read Kunden suchen (q, status, paginiert)
POST /customers customers customer:write Kunde anlegen (409 bei möglichen Dubletten ohne acknowledgeDuplicates)
GET /customers/{id} customers customer:read Kunde inkl. Ansprechpartner
PATCH /customers/{id} customers customer:write Kunde ändern (fehlt = unverändert, null = leeren)
GET /sites sites site:read Standorte suchen (q, customerId, status, paginiert)
POST /sites sites site:write Standort anlegen
GET /sites/{id}/history sites site:read Einsatzhistorie (Außendienst: nur freigegebene Einsätze)
GET /work-orders work_orders Scope (work_order:read_all/read_team) Auftragsliste mit Filtern/Presets
POST /work-orders work_orders Service: work_order:write (Notfall: emergency:create) Auftrag anlegen
GET /work-orders/{id} work_orders Scope Detail + availableTransitions + completionBlockers
PATCH /work-orders/{id} work_orders Service: work_order:write Stammdaten ändern (baseVersion)
POST /work-orders/{id}/assign work_orders work_order:assign Team/Monteure zuweisen
POST /work-orders/{id}/transition work_orders je Übergang (requiredPermission) Statuswechsel (422 blocked mit Blockern)
GET /work-orders/{id}/materials work_orders Scope Material Soll/Ist
POST /work-orders/{id}/materials work_orders work_order:write Materialvorgabe hinzufügen
POST /work-orders/{id}/documents work_orders document:write Dokument hochladen (multipart)
POST /work-orders/{id}/daily-report reports report:write Tagesbericht-Entwurf anlegen/holen (201/200)
POST /work-orders/{id}/completion-report reports report:write Abschlussbericht-Entwurf anlegen/holen (422 bei Blockern)
POST /work-orders/import imports import:write Auftragsdokument importieren (multipart)
GET /imports/{id} imports import:write Importstatus, Extraktion, Kandidaten
POST /imports/{id}/confirm imports import:write, work_order:write Prüfformular bestätigen → Auftrag
POST /reports/{id}/approve reports report:read + Service: report:approve_team/report:approve Bericht freigeben
GET /reports/{id}/pdf reports report:read PDF des freigegebenen Berichts (?download=1)
GET /reports/{id}/files/{documentId} reports report:read Foto/Unterschrift/Logo aus dem Bericht
POST /sync field Service je opType (field:execute, emergency:create, …) Batch-Operationen (offline/online)
POST /uploads field field:execute Foto/Sprachnotiz hochladen → documentId
GET /field/bundle field field:execute Offline-Pull (?since=<ISO>, max. 200 Aufträge)
GET /field/documents/{id} field Service: document:read + Sichtbarkeit/Scope Dokument für die Mobile-App (?variant=preview)
GET /openapi.json – angemeldet OpenAPI-3.1-Dokument