12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-12 18:32:33 +02:00
parent 405df0a122
commit 5b8bdf4182
779 changed files with 480564 additions and 6241 deletions

View file

@ -0,0 +1,241 @@
# 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://presseportale.com/account/tokens",
"docs_url": "https://presseportale.com/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
```