presseportale/dev/migration 2026/07-API-MIGRATION.md
Kevin Adametz 0a3e52d603 19-05-2026 Rebrand Pressekonto, Hub-Flux UI und Legacy-Media-Migration
Umbenennung presseportale → pressekonto in Domains, Themes und Dokumentation.
Design-Tokens, Portal-Shell, Customer-Dashboard, Auth- und Admin-PM-Views.
Artisan-Befehl migrate:legacy-media mit Tests und Hub-Flux-Entwicklungsdocs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 16:36:13 +00:00

241 lines
9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```