Files
craftvia/docs/craftvia/API.md
T
msolarczekandClaude Opus 5 d9290a187c L15 Testphase & Onboarding: Selbstanmeldung mit Double-Opt-in, Plattform-Wizard, Nur-Lesen-Sperre, Export, Lebenszyklus-Job
- Datenmodell: Testphasen-Lebenszyklus am Mandanten (plan, trialEndsAt, readOnlySince, deletionDueAt,
  Versandmarker), TrialSignup (Plattform, Hashes statt Klartext), TenantExport (RLS), Onboarding-Status
- /testen: 5-Schritte-Wizard (Betrieb, Admin-Konto, Enddatum, Einrichtung, Zusammenfassung),
  Bestätigung per POST, direkte Anmeldung über login-ticket; Rate-Limit je IP/E-Mail, Honeypot,
  Enumeration-Schutz, Slug-Kollisionen
- Plattform: Wizard „Testmandant anlegen“ mit Einladung, Badges/Filter, Enddatum ändern,
  umwandeln, beenden, Löschung vormerken/abbrechen (Bestätigung + Audit)
- Schreibsperre nach Ablauf zentral in moduleGuard und requireApiContext (non-GET über withApi),
  Upload-Routen, Einstellungen/Nutzerverwaltung, Worker-Jobs; Banner Backoffice + mobil
- Datenexport (ZIP mit CSV/JSON + Dateien) als Worker-Job, auch im Nur-Lesen-Zustand
- Täglicher Job trial-lifecycle: Erinnerungen 7/3/1, Ablauf, Löschhinweis, Löschung über das Offboarding
- Erste-Schritte-Checkliste im Dashboard, Mail-Vorlagen de/en, Tests + Smoke, Betriebsdoku

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

9.5 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 /planning/board work_orders Service: work_order:read_all oder Teamleiter (eigene Teams) Plantafel: Aufträge je Team/Tag, Kapazität, Auslastung, Konflikte (from, to ≤ 42 Tage, teamId, orderTypeId, priority)
POST /planning/schedule work_orders work_order:assign + Service: work_order:write Einplanen (Team + Termin + Dauer, atomar, baseVersion Pflicht, 409 „Auftrag wurde zwischenzeitlich geändert“)
GET /planning/recommendations work_orders work_order:assign Einsatz-Empfehlungen (Luftlinie + freie Kapazität, Top 5) + nahe ungeplante Aufträge; nur Vorschläge
GET /planning/live work_orders Service: work_order:read_all oder Teamleiter Live-Lage: Status aus aktiver WorkSession, Standort = Objekt des Auftrags, keine Geräte-Koordinaten
GET /openapi.json – angemeldet OpenAPI-3.1-Dokument

Testphase: Nur-Lesen nach Ablauf (L15)

Für abgelaufene Testmandanten beantworten alle nicht lesenden Anfragen (POST/PUT/PATCH/DELETE, inkl. /sync und /uploads) mit 422 { "error": { "code": "blocked", "message": "trial_expired", "details": { "readOnly": true, "message": "…", "deletionDueAt": "…" } } }. GET-Anfragen, Datei-Downloads und PDFs bleiben erlaubt. Siehe TESTPHASE.md.