# 07 – API-Migration ## 1. Ausgangslage (Legacy) - Authentifizierung: Statischer `api_key`-String im User-Profil (`sfGuardUserProfile.api_key`). - Transport via HTTP-Header oder Query-Param, je nach Client (historisch gewachsen). - Symfony-Module `pressrelease`, `pressreleaseimage`, `company`, `category`, `newsletter` stellten Actions bereit (ApiActions-Basisklasse). - Format: **JSON** (teils auch XML) – wurde via `renderText(json_encode(...))` erzeugt. - CSRF für API-Requests in `prepareForm()` abgeschaltet. - **Undokumentiert** – Schnittstellen-Wissen liegt in Server-Logs und im Symfony-Code verstreut. Vor der finalen Migration: **Zugriffs-Log des Prod-Servers der letzten 3 Monate** analysieren, um vollständige Liste der tatsächlich genutzten Endpoints zu erhalten. (Aufgabe zu Beginn Phase 7.) --- ## 2. Ziel-Architektur - **URL-Prefix**: `/api/v1` - **Auth**: **Nur Laravel Sanctum Personal Access Tokens** (Bearer). - ⭐ **Entscheidung D-05 / Q-03:** Kein Kompatibilitätsmodus für Legacy-Keys. Cut-over statt Keep-Alive. - **Format**: JSON only; `Accept: application/json` empfohlen. Validation-Fehler als RFC 7807 (Laravel Default). - **Versionierung**: `v1` = neuer Stand, saubere Ressourcen (kein Legacy-Feld-Alias mehr nötig, weil Clients eh umstellen müssen). - **Rate-Limiting**: Pro Sanctum-/Bearer-Token 60/min. - **Dokumentation**: OpenAPI-YAML (`docs/api/v1.yml`) + optional Scribe-Renderer. ### Token-Scopes (Sanctum Abilities) | Ability | Zweck | |---|---| | `press-releases:read` | Listen + Details lesen | | `press-releases:write` | Erstellen / Update / Delete | | `press-release-images:write` | Upload / Delete | | `companies:read` | Firmen lesen | | `newsletter:subscribe` | Newsletter-Opt-In | Token können im Customer-Portal (und Admin) mit individuellen Scopes erstellt werden. --- ## 3. Cut-Over-Strategie für Legacy-API-Clients **Kein Parallelbetrieb.** Kommunikation in 3 Phasen: | Phase | Zeitpunkt | Aktion | |---|---|---| | **Ankündigung** | T-30 Tage | Mail an alle User mit `api_key_legacy != null`: neue Technik kommt, Token-Migration nötig, Link zur Doku | | **Erinnerung** | T-7 Tage | Zweite Mail | | **Cut-over** | Go-Live | Alter `api_key` wird abgewiesen. Response `410 Gone` mit `message: "Legacy API keys are no longer supported."`, `migration_url` und `docs_url` | Aktive Kunden bekommen im Backend / Customer-Portal eine prominent platzierte Migrations-Anleitung + Token-Generator. --- ## 4. Endpoint-Mapping (vorläufig) | Legacy-Route | Methode | Neu | Ressource | |---|---|---|---| | `pressrelease/create` | POST | `POST /v1/press-releases` | `PressReleaseResource` | | `pressrelease/list` | GET | `GET /v1/press-releases?page=&status=` | collection | | `pressrelease/show/:id` | GET | `GET /v1/press-releases/{id}` | | | `pressrelease/update/:id` | POST | `PATCH /v1/press-releases/{id}` | | | `pressrelease/delete/:id` | POST | `DELETE /v1/press-releases/{id}` | | | `pressreleaseimage/upload` | POST | `POST /v1/press-releases/{id}/images` | `multipart/form-data`, max. 5 MB | | `pressreleaseimage/list` | GET | `GET /v1/press-releases/{id}/images` | collection | | `pressreleaseimage/delete/:id` | POST | `DELETE /v1/press-release-images/{id}` | | | `company/list` | GET | `GET /v1/companies` | | | `company/show/:id` | GET | `GET /v1/companies/{id}` | | | `category/list` | GET | `GET /v1/categories?lang=de\|en` | | | `newsletter/subscribe` | POST | `POST /v1/newsletter/subscribe` | | | `newsletter/unsubscribe` | – | **nicht Teil von P7** | wird später im neuen Frontend mit sauberem Newsletter-Dienst neu implementiert | ### Beispiel: PressReleaseResource ```php return [ 'id' => $this->id, 'uuid' => $this->uuid, 'legacy' => [ 'portal' => $this->legacy_portal, 'id' => $this->legacy_id, ], 'portal' => $this->portal->value, 'title' => $this->title, 'slug' => $this->slug, 'language' => $this->language, 'status' => $this->status->value, 'text' => $this->text, 'backlink_url' => $this->backlink_url, 'keywords' => $this->keywords, 'teaser' => [ 'begin' => $this->teaser_begin, 'end' => $this->teaser_end, ], 'category' => CategoryResource::make($this->whenLoaded('category')), 'company' => CompanyResource::make($this->whenLoaded('company')), 'images' => PressReleaseImageResource::collection($this->whenLoaded('images')), 'published_at' => $this->published_at?->toIso8601String(), 'created_at' => $this->created_at->toIso8601String(), ]; ``` --- ## 5. Authentifizierung (Sanctum only) Route-Definition: ```php Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () { Route::apiResource('press-releases', PressReleaseController::class); Route::get('press-releases/{pressRelease}/images', [PressReleaseImageController::class, 'index']); Route::post('press-releases/{pressRelease}/images', [PressReleaseImageController::class, 'store']); Route::delete('press-release-images/{pressReleaseImage}', [PressReleaseImageController::class, 'destroy']); Route::apiResource('companies', CompanyController::class)->only(['index', 'show']); Route::get('categories', [CategoryController::class, 'index']); Route::post('newsletter/subscribe', [NewsletterController::class, 'subscribe']); }); ``` Abilities werden pro Controller-Action geprüft: ```php $request->user()->tokenCan('press-releases:write') or abort(403); ``` ### Legacy-Handler (One-Shot) Ein kleiner Handler fängt Requests mit `api_key`-Param ab und antwortet mit einer freundlichen Fehlermeldung inkl. Link zur Migrationsdoku (statt generischem 401): ```php if ($request->has('api_key') || $request->header('X-Api-Key')) { return response()->json([ 'message' => 'Legacy API keys are no longer supported.', 'migration_url' => url('/account/tokens'), 'docs_url' => url('/docs/api/v1'), ], 410); // 410 Gone – signalisiert deutlich: "Feature ist weg, hier ist der Ersatz" } ``` --- ## 6. Authorization (Policies) - `PressReleasePolicy::create` → User muss `press-releases:write`-Ability UND Mitglied einer Company mit `role` ∈ {owner, responsible} sein. - `PressReleasePolicy::update/delete` → Ownership + Status-Check (nur `draft` / `rejected` editierbar). - `PressReleaseImageController` → List nur eigene Pressemitteilung, Upload/Delete nur eigene `draft` / `rejected` Pressemitteilung, Upload max. 5 MB. - `NewsletterController::subscribe` → öffentlich, aber `newsletter:subscribe`-Ability erforderlich bei authentifiziertem Zugriff. - `newsletter/unsubscribe` → bewusst nicht in P7 umgesetzt; neuer Newsletter-Dienst folgt nach der Migration. - Fehlermeldungen sprachlich konsistent (deutsch im `message`-Feld, abhängig von `Accept-Language`). --- ## 7. Fehlerformat Standard Laravel: ```json HTTP/1.1 422 Unprocessable Entity Content-Type: application/json { "message": "The given data was invalid.", "errors": { "title": ["Der Titel wird benötigt."], "text": ["Der Text darf nicht leer sein."] } } ``` Legacy-Key-Versuch: ```json HTTP/1.1 410 Gone Content-Type: application/json { "message": "Legacy API keys are no longer supported.", "migration_url": "https://pressekonto.de/account/tokens", "docs_url": "https://pressekonto.de/docs/api/v1" } ``` --- ## 8. Versioning & Zukunft - `v1` = neuer Stand ab Go-Live (keine Legacy-Altlasten). - `v2` erst auf Anfrage / bei breaking changes. --- ## 9. Testing - **Feature-Tests** pro Endpoint: - Happy-Path mit Sanctum-Token (mit passender Ability) - Auth-Fehlerfall (kein Token) - Ability-Fehlerfall (403) - **Legacy-Key-Versuch → 410** mit migration_url - Validation-Fehler (422) - Policy-Fehler (403) - **Smoke-Test**-Skript (`php artisan api:smoke`) das gegen Staging 10 kritische Endpoints fährt. --- ## 10. OpenAPI-Dokumentation Ablegen unter `docs/api/v1.yml`, generiert aus: - Scribe (`knuckleswtf/scribe`) – PHP-Annotations → YAML, oder - manuell (konsistenter, wenn Team es pflegt). **Empfehlung**: Mit Scribe starten, bei Bedarf manuell überschreiben. --- ## 11. Migrationsschritte in Phase 7 1. Log-Analyse: alle in den letzten 90 Tagen in Produktion aufgerufenen URLs auflisten 2. Endpoints priorisieren (Top-20 zuerst) 3. Routen, Controller, Resources, Requests, Policies pro Endpoint 4. Sanctum-Ability-Seeder für Standard-Scopes 5. Legacy-410-Handler 6. Integrationstests 7. Smoke-Tests gegen Staging mit echtem Client 8. **Kommunikation an API-Partner** (E-Mail-Verteiler) mit Token-Migrations-Anleitung (T-30, T-7, T-0) 9. Self-Service Token-Management im Customer-Portal verfügbar machen ### 11.1 Log-Analyse-Tooling Für Schritt 1 gibt es ein wiederholbares Artisan-Werkzeug: ```bash php artisan api:analyze-legacy-access-logs /var/log/nginx/access.log "/var/log/nginx/access.log.*" --top=20 ``` Der Command erkennt Legacy-API-Routen (`pressrelease/*`, `pressreleaseimage/*`, `company/*`, `category/*`, `newsletter/*`), zählt Requests mit `api_key`, maskiert Beispielrequests und speichert nur API-Key-Fingerprints statt Klartext-Keys. Der JSON-Report landet unter: ```bash storage/app/private/migration/legacy-api-access-*.json ```