presseportale/dev/betrieb/api-legacy-paritaet.md
Kevin Adametz 24184f1dec api: Bildupload v1 unter Formular v2 (Pflichtfelder, Kennung des Wortlauts, Bestätigung mit Token), Legacy-Bildupload abgeschaltet, API-Doku (Bildkennzeichnung Block C)
- StorePressReleaseImageRequest: copyright, rights_confirmed, confirm_text_version Pflicht; 422 mit aktueller Kennung; 16 MiB
- PressReleaseImageController über den gemeinsamen Uploader, Löschen über den Remover; Resource mit Portalkennzeichnung, Kundenangabe, Herkunft, Bestätigung und warnings[]
- POST /service/pressreleases/{id}/images antwortet 410 mit Hinweis auf v1 (E-B4)
- API-Seite und Sprachdateien: Felder und Rechtehinweis mit aktiver Textkennung; Paritätsdokument ergänzt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 09:35:52 +00:00

160 lines
15 KiB
Markdown
Raw 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.

# API-Legacy-Parität — Arbeitsdokument (Stand 10.07.2026)
> **Ziel (User-Vorgabe 10.07.):** Beim Cutover muss die API-Schnittstelle für
> Bestandskunden **erst einmal genauso funktionieren wie vorher**. Dieses
> Dokument hält den belegten Alt-Vertrag fest, listet die Paritäts-Lücken und
> führt die einzelnen Schritte als Checkliste. Testdaten-Wünsche des Users
> folgen (Abschnitt „Testdaten", unten).
**Quellenlage:** Der komplette Alt-Code liegt im Repo unter
`_businessportal24.com/` (Symfony 1.4). Analysiert am 10.07.:
`apps/frontend/config/routing.yml` (service_-Routen),
`plugins/PressePortalPlugin/modules/pressrelease/lib/pluginPressReleaseActions.class.php`,
`modules/pressreleaseimage/actions/actions.class.php`,
`modules/company/actions/actions.class.php`, `lib/ApiActions.class.php`,
`lib/filters/ApiKeyGuardAuthFilter|SecureApiAccessFilter|ApiAccessFilter`,
`modules/tnHttpError/templates/*.json.php`, JSON-Templates der Module.
---
## 1. Belegter Alt-Vertrag (aus dem Symfony-Code)
### Authentifizierung & Zugriff
- `X-ApiKey`-Header (Filter `ApiKeyGuardAuthFilter` liest `sfGuardUserProfile.api_key`); Web-Session konnte API-Routen ebenfalls nutzen.
- `GET/HEAD` verlangt Credential `apiReadAccess`, `POST/PUT/DELETE` verlangt `apiWriteAccess`, andere Methoden → 405 (`SecureApiAccessFilter`).
- `X-ApiKey` auf Nicht-API-Routen → 501 (`ApiAccessFilter`).
- CSRF für API-Requests deaktiviert.
### Endpoints (12 API-Routen laut routing.yml, alle mit `.json`-Suffix!)
| Alt-URL (Methode) | Verhalten |
|---|---|
| `POST /service/pressreleases.json` | PM anlegen. Params **verschachtelt**: `press_release[company_id]` (Pflicht), `press_release[category_id]`, `press_release[language]`, `press_release[title]`, `press_release[text]`, `press_release[contacts_list][]`, ggf. `press_release[backlink_url]`. **Status-Logik:** Standard `new` (= Redaktions-Queue); hatte die Firma `hasActivePerPresskitPayment()` oder der User die Permission `immediatePublication` → **sofort `published`**. Response: `201` + `Location`-Header auf die Show-URL, Body `{}`. Es gab **keinen separaten Submit-Call** — anlegen = einreichen. |
| `GET /service/pressreleases.json` | **Öffentlicher Index**: nur `status=published`, portalweit (NICHT auf den User beschränkt!), Filter `query[languages][]`, `query[categories][]`, `query[offset]`, `query[limit]`, sortiert `created_at DESC`. Response: **JSON-Array von Show-URLs** (Strings), keine Objekte. |
| `GET /service/pressreleases/{id}.json` | Nur `published` (sonst 404; inaktive Firma → 404). Zählt Hits (session-dedupliziert). Response: flaches Objekt `{id, title, text, teaser_end, language, category_id, view_url, company: {viewUrl, name}}`. |
| `GET /service/pressreleases/{id}/images.json` | Nur bei `published` PM. Response: **Array von Bild-Show-URLs**. |
| `POST /service/pressreleases/{id}/images.json` | Nur Eigentümer (sonst 403 „You are not the creator …"). Multipart, Felder verschachtelt `press_release_image[image]` (+ title/description/copyright). Response: `201` + `Location`, Body `{}`. **Seit Bildkennzeichnung E-B4 (22.09.2026) mit `IMAGE_RIGHTS_V2=true` abgeschaltet: `410` mit Hinweis auf v1** — der Alt-Vertrag kennt weder Bildnachweis noch Rechtebestätigung und wurde nie genutzt. |
| `GET /service/pressreleases/{id}/images/{imgId}.json` | Response: `{id, title, description, copyright, image_file_url}`. |
| `GET /service/users/me/companies.json` | Eigene Firmen; `limit`/`offset` direkt ODER `query[limit]`/`query[offset]`. Response: **Array von Show-URLs**. |
| `POST /service/users/me/companies.json` | Firma anlegen (`201` + `Location`, Body `{}`). |
| `GET /service/users/me/companies/{id}.json` | Nur eigene (sonst 403). |
| `GET/POST /service/users/me/companies/{id}/contacts.json` + `GET …/contacts/{id}.json` | Pressekontakte lesen/anlegen. |
### Fehlerformat
- 422: `{ "global_errors": [...], "field_errors": {...} }`
- andere Fehler: `{ "global_errors": [...] }`
---
## 2. Ist-Stand der neuen Aliasse (routes/legacy-api.php)
**Schon da:** Key-Import (`legacy:import-api-keys`, 406 Tokens), Auth-Übersetzung
X-ApiKey/api_key → Bearer, unbekannte Keys 410 + Hinweis, Rate-Limit,
Eligibility-Checks, Alias-Routen für pressreleases (GET/POST), pressreleases/{id}
(GET), images (GET/POST), users/me/companies (GET, Show), Rest → 410-Fallback.
**Aber:** Die Aliasse zeigen direkt auf die v1-Controller — Request- und
Response-Verträge sind v1, nicht Alt. Für „funktioniert genauso wie vorher"
fehlt eine Kompatibilitätsschicht.
## 3. Paritäts-Lücken (Befund 10.07.)
| # | Lücke | Wirkung auf Alt-Clients |
|---|---|---|
| L1 | **`.json`-Suffix**: Alt-URLs enden auf `.json`, unsere Aliasse matchen nur ohne Suffix | Calls wie `/service/pressreleases.json` landen im 410-Fallback → **Totalausfall** |
| L2 | **Verschachtelte Params**: `press_release[...]` / `press_release_image[...]` / `query[...]` vs. flache v1-Felder | POST/Filter validieren nicht → 422 → **Anlegen unmöglich** |
| L3 | **Kein Auto-Submit**: alt = anlegen ist einreichen (`new` → Redaktion bzw. sofort `published`); neu = Draft, Submit-Endpoint existiert im /service-Raum gar nicht | PMs bleiben unsichtbar als Entwürfe liegen → **Kernfunktion kaputt** |
| L4 | **Index-Semantik**: alt = öffentliche published-Liste (portalweit) als URL-Array; neu = eigene PMs als Objekt-Collection | Clients, die den Index parsen/crawlen, brechen |
| L5 | **Show-Shape**: alt = flaches Objekt mit `view_url`, `company.viewUrl`; neu = v1-Resource (andere Felder/Nesting), owner-scoped statt published-only | Parser brechen; nicht-eigene published PMs sind nicht mehr abrufbar |
| L6 | **Create-Response**: alt = `201` + `Location` + `{}`; neu = `201` + volle Resource | vermutlich unkritisch (Location fehlt aber) |
| L7 | **Fehlerformat**: alt = `global_errors`/`field_errors`; neu = Laravel `message`/`errors` | Fehlerbehandlung der Clients bricht (weich) |
| L8 | **Nicht abgebildete Endpoints**: Company-CREATE, Presscontact-Index/Create/Show, Einzelbild-GET → heute 410 (bewusste Entscheidung 06.07.) | Clients, die Firmen/Kontakte per API anlegen, brechen |
| L9 | KI-Deklarations-Default für /service ist umgesetzt (none/original, 10.07.) — bei Auto-Submit (L3) mitdenken | erledigt, nur verdrahten |
**Einordnung:** Der bisherige „end-to-end verifiziert (200)"-Test vom 06.07.
prüfte Auth + Statuscode der suffix-losen Pfade, nicht den Alt-Vertrag.
Verhaltensparität ist mit dem Alt-Code jetzt exakt herstellbar.
---
## 3a. Log-Auswertung Prod (User hat Logs geliefert → `dev/api/`, 10.07.)
Strukturierte Aktivitäts-Logs beider Portale (Mai–Juli 2026, `b24_activity` +
`pe_activity`; 8.622 erfolgreiche API-Auths, 619 Publikationen). Ergebnis:
- **Genau EIN echter Client: connektar** (`connekt.connektar.de`, User `info@connektar.de`, Legacy-ID 11768 auf bp24). Daneben nur eine tote Integration mit ungültigem Key (login_failed, kein User-Agent).
- **Tatsächlich genutzte Endpoints (alle OHNE `.json`-Suffix!):**
| Calls | Endpoint | Status heute |
|---|---|---|
| 5.935 | `POST /service/users/me/companies` | **410 — fehlt!** |
| 976 | `POST /service/users/me/companies/{id}/contacts` | **410 — fehlt!** |
| 594 | `POST /service/pressreleases` | Alias vorhanden, Params/Verhalten abweichend (L2/L3) |
| 594 | `GET /service/pressreleases/{id}` | Alias vorhanden, Response/Sichtbarkeit abweichend (L5) |
| 523 | `POST /service/pressreleases/{id}/images` | Alias vorhanden, Params abweichend (L2) |
- **`pressrelease_published` feuert direkt beim POST** — connektars PMs gehen **sofort live** (Sofort-Publish-Privileg bestätigt; Flow: Firma anlegen → Kontakte → PM (sofort published) → Bilder → GET zur Kontrolle).
- **Damit entschärft:** L1 (`.json`-Suffix wird real nicht genutzt), L4 (kein Index-Call). **Verschärft:** L8 — Company-/Kontakt-Create sind die meistgenutzten Calls überhaupt und heute tot.
## 3b. Entscheidungen des Users (10.07.)
1. **Zwei APIs:** Die Legacy-API (`/service/…`) bildet für Bestandskunden das Alt-Verhalten ab; die v1-API bleibt die neue, saubere Schnittstelle mit den neuen Funktionen (KI-Deklarationsfelder etc.).
2. **Sofort-Publish nachbilden + Admin-Schalter:** Standard = eingehende Legacy-PMs werden wie im Altsystem sofort veröffentlicht. Über einen **Schalter im Admin** kann stattdessen die KI-Prüfung aktiviert werden: dann Status review → KI routet (Gelb/Grün sofort veröffentlichen, Rot nicht).
3. **Rückmeldung bei Ablehnung:** War im Altsystem nicht vorhanden (connektar pollt GET — 404 bis published). Ablehnungs-Mail an den Einlieferer existiert im neuen System bereits (`notifyAuthor`); ein maschinenlesbarer Rückkanal (Status-Feld/Webhook) kommt **später als Feature der neuen API** — Integration hier nur vormerken, nicht Teil der Parität.
---
## 4. Schritte (Checkliste — hier abhaken + dokumentieren)
Scope nach Log-Auswertung (§3a): **die 5 connektar-Endpoints**, verhaltensgleich.
- [x] **S1 — Legacy-Compat-Controller `/service/…`** (ersetzt die direkten v1-Aliasse):
- [x] `POST /service/users/me/companies` — Felder `company[name|address|country_id|phone|fax|email|website]` (+ `logo`-Datei); legacy `country_id` → neuer `country_code`; Response `201` + `Location` + `{}`; Alt-Fehlerformat.
- [x] `POST /service/users/me/companies/{id}/contacts` — Felder `contact[salutation_id|title|first_name|last_name|responsibility|phone|fax|email]`; Zugriff nur eigene Firma (403-Wortlaut s. §1); Response wie oben.
- [x] `POST /service/pressreleases` — Felder `press_release[company_id|contacts_list[]|category_id|language|title|text|backlink_url|…]`; Statusverhalten s. S2; KI-Deklarations-Default none (erledigt 10.07.); Response `201` + `Location` + `{}`.
- [x] `POST /service/pressreleases/{id}/images` — Multipart `press_release_image[image|title|description|copyright]`; Eigentümer-Check (403 „You are not the creator of this press release"); ai_declaration-Default original (erledigt 10.07.); Response `201` + `Location` + `{}`.
- **22.09.2026:** hinter `IMAGE_RIGHTS_V2` abgeschaltet (410, Bildkennzeichnung E-B4). Der Default „original“ war eine erfundene Erklärung (Decision-Update S5).
- [x] `GET /service/pressreleases/{id}` — nur `published` (sonst 404 — connektars Polling-Kontrolle!), Hit-Zählung, flaches Alt-Objekt `{id,title,text,teaser_end,language,category_id,view_url,company:{viewUrl,name}}`.
- [x] Alt-Fehlerformat überall: `{"global_errors":[…],"field_errors":{…}}` (422) bzw. `{"global_errors":[…]}`.
- [x] Pest-Paritäts-Tests (`LegacyServiceParityTest`) — je Endpoint gegen den §1-Vertrag (inkl. connektar-Flow end-to-end: Firma → Kontakt → PM → Bild → GET).
- Nicht nachgebaut (real ungenutzt, bleiben 410): GET-Index-Routen, Einzelbild-GET, Company-GET/Show, Kontakt-GET, `.json`-Suffix-Formen. GET users/me/companies + Show bleiben als vorhandene Aliasse bestehen (real nur von der toten Integration gecallt, schadet nicht).
- [x] **S2 — Statusverhalten create (Entscheidung 10.07., §3b):** Default **Sofort-Publish** (Alt-Verhalten, per Log belegt); **Admin-Schalter** „KI-Prüfung für Legacy-API" → Status review + KI-Routing (Gelb/Grün auto-publish, Rot Ablehnung + Mail). Umsetzung: Setting (config + Admin-UI-Toggle), Legacy-Create wertet es aus. Blacklist/Slot-Verbrauch laufen in beiden Wegen über `PressReleaseService::publish()`.
- [x] **S3 — Endpoint-Scope entschieden (10.07.):** per Log-Auswertung (§3a) — nur die 5 echten connektar-Endpoints nachbauen, Rest bleibt 410. (User hat Logs in `dev/api/` geliefert; ersetzt das frühere [EXTERN] Log-Beschaffen.)
- [x] **S4 — Testdaten-Command gebaut (10.07.):** `php artisan legacy:seed-api-testdata [--portal=…] [--email=…]` — legt den API-Testkunden nach connektar-Muster an (aktiv, grandfathered, Legacy-Key via Importer, Sofort-Publish-Permission, Firma + Pressekontakt) und gibt Key + curl-Sammlung des kompletten Flows aus; idempotent (wiederholter Lauf erneuert nur den Key), gesperrt in Produktion. **Live verifiziert 10.07.:** kompletter Flow per curl gegen die lokale App (Firma 201 → Kontakt 201 → PM 201 + sofort published → GET 200 mit Alt-Shape). Weitere Testdaten-Wünsche des Users folgen (Abschnitt 5).
- [ ] **S5 — Paritäts-Smoke auf Staging:** curl-Sammlung des connektar-Flows (X-ApiKey, verschachtelte Params) gegen Staging; Vergleich gegen Alt-System-Responses.
- [ ] **S6 — Rückkanal Ablehnung (nachgelagert, neue API):** Status-Feld/Webhook in v1 für „abgelehnt" — Design offen; bis dahin gilt Alt-Verhalten (GET pollt, 404 = nicht veröffentlicht) + Ablehnungs-Mail.
- [ ] **S7 — R6.4-Mailing ergänzen:** Legacy-Keys laufen weiter, Verhalten unverändert (Sofort-Publish bzw. KI-Schalter), KI-Deklarations-Default (Erklärungswirkung), Hinweis auf neue v1-API.
## 5. Testdaten (Platzhalter — Vorgaben vom User folgen)
> User (10.07.): „hier hätte ich gerne auch selber ein paar Testdaten — dazu
> gleich mehr". Sobald die Wünsche da sind, hier dokumentieren (welche Kunden,
> welche PMs/Firmen, welcher Kanal: /service-Alt-Calls und/oder v1) und in S4
> umsetzen.
## 6. Protokoll
- **10.07.2026:** Dokument angelegt; Alt-Vertrag vollständig aus dem
Symfony-Code extrahiert (§1); Lücken L1–L9 identifiziert (§3). Vorarbeiten
heute bereits erledigt: KI-Deklarations-Defaults für /service (L9),
Dev-Queue-Fix (KI-Pipeline lokal lauffähig).
- **10.07.2026 (2):** User liefert Prod-Aktivitäts-Logs (`dev/api/`) →
Auswertung §3a: ein Client (connektar), 5 Endpoints, Sofort-Publish belegt;
zwei der fünf laufen heute auf 410. User-Entscheidungen §3b festgehalten
(Sofort-Publish + Admin-Schalter KI-Prüfung, zwei getrennte APIs,
Rückkanal später). Schrittplan S1–S7 auf den echten Scope zugeschnitten;
S3 damit entschieden. **Nächster Schritt: S1 + S2 implementieren.**
- **10.07.2026 (3): S1 + S2 UMGESETZT** — eigene Legacy-Controller
(`app/Http/Controllers/Api/Legacy/`) für die 5 connektar-Endpoints,
verhaltensgleich (verschachtelte Params, 201+Location+{}, Alt-Fehlerformat,
published-only-GET mit Hit-Zählung, Legacy-Land-Mapping). Anlegen =
Einreichen; Sofort-Publish über neue Permission `api:immediate-publication`
(Seeder; **connektar-User beim Go-Live zuweisen, WS-7!**) — übersteuerbar
durch den neuen Admin-Schalter „KI-Prüfung Legacy-API" (`app_settings`,
Toggle auf /admin/press-releases). Dazu: KI-Spalte + manueller Prüf-Button
(ohne Routing) in der Admin-PM-Liste; Benachrichtigungs-Mails bei
API-Einlieferern an Pressekontakt, sonst Firmen-Mail, Fallback Einlieferer
(Entscheidung 10.07.). Tests: `LegacyServiceParityTest` (5, inkl.
connektar-Flow end-to-end) + Bestands-Tests nachgezogen.
- **Offen:** S4 Testdaten (User-Vorgaben ausstehend) · S5 Staging-Smoke ·
S6 Rückkanal (v1, nachgelagert) · S7 Mailing · WS-7 Permission connektar.