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