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>
241 lines
9 KiB
Markdown
241 lines
9 KiB
Markdown
# 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
|
||
```
|