API-Doku: OpenAPI-Spec aus public/ statt geloeschtem docs/ ausliefern
Der docs/-Mount kam nicht mit; die Route /docs/api/v1 lief dadurch auf 500. Liest jetzt public/api/v1.yml mit 404-Guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f11e8aab7c
commit
07e0bc598e
2 changed files with 318 additions and 1 deletions
313
public/api/v1.yml
Normal file
313
public/api/v1.yml
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: pressekonto Publisher API
|
||||||
|
version: "1.0"
|
||||||
|
description: |
|
||||||
|
REST-API des Publisher-Hubs (pressekonto) für presseecho.de und
|
||||||
|
businessportal24.com. Pressemitteilungen werden als Entwurf angelegt,
|
||||||
|
optional mit Bildern versehen und über die Submit-Route zur
|
||||||
|
Inhaltsprüfung eingereicht — eine direkte Veröffentlichung über die
|
||||||
|
API ist bewusst nicht möglich.
|
||||||
|
|
||||||
|
Authentifizierung über einen persönlichen API-Token (Bearer). Jeder
|
||||||
|
Token trägt Berechtigungen (Abilities), die den Zugriff auf die
|
||||||
|
Endpunkte steuern. Tokens werden im Publisher-Konto unter „API-Tokens"
|
||||||
|
selbst erstellt und widerrufen (Voraussetzung: aktive Buchung).
|
||||||
|
Rate-Limit: 60 Anfragen pro Minute je Token (Header X-RateLimit-Limit /
|
||||||
|
X-RateLimit-Remaining, bei 429 zusätzlich Retry-After).
|
||||||
|
|
||||||
|
Migration: Bestehende API-Schlüssel der Altportale wurden übernommen
|
||||||
|
und bleiben gültig — auch in der alten Übergabeform (X-ApiKey-Header)
|
||||||
|
und auf den dokumentierten /service/…-Pfaden. Unbekannte Alt-Schlüssel
|
||||||
|
erhalten 410 mit Migrationshinweis. Neue Integrationen nutzen bitte
|
||||||
|
Bearer-Tokens auf /api/v1.
|
||||||
|
servers:
|
||||||
|
- url: https://pressekonto.de/api/v1
|
||||||
|
security:
|
||||||
|
- bearerToken: []
|
||||||
|
tags:
|
||||||
|
- name: press-releases
|
||||||
|
description: "Eigene Pressemitteilungen (Ability: press-releases:read bzw. press-releases:write)"
|
||||||
|
- name: images
|
||||||
|
description: "Bilder einer Pressemitteilung (Ability: press-release-images:write zum Schreiben)"
|
||||||
|
- name: companies
|
||||||
|
description: "Eigene Firmenprofile/Newsrooms (Ability: companies:read)"
|
||||||
|
- name: categories
|
||||||
|
description: "Aktive Rubriken (Ability: press-releases:read)"
|
||||||
|
- name: newsletter
|
||||||
|
description: "Newsletter-Anmeldungen (Ability: newsletter:subscribe)"
|
||||||
|
paths:
|
||||||
|
/press-releases:
|
||||||
|
get:
|
||||||
|
tags: [press-releases]
|
||||||
|
summary: Eigene Pressemitteilungen auflisten
|
||||||
|
parameters:
|
||||||
|
- name: status
|
||||||
|
in: query
|
||||||
|
description: Nach Status filtern (draft, review, published, rejected, archived)
|
||||||
|
schema: { type: string }
|
||||||
|
- name: per_page
|
||||||
|
in: query
|
||||||
|
description: Seitengröße, Standard 25, max. 100
|
||||||
|
schema: { type: integer, maximum: 100 }
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
schema: { type: integer }
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Paginierte Liste
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/PressRelease" }
|
||||||
|
post:
|
||||||
|
tags: [press-releases]
|
||||||
|
summary: Pressemitteilung anlegen (immer als Entwurf)
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/PressReleaseInput" }
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Entwurf angelegt
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data: { $ref: "#/components/schemas/PressRelease" }
|
||||||
|
"403": { description: "Ability fehlt oder Firma gehört nicht zum Konto" }
|
||||||
|
"422": { description: "Validierungsfehler" }
|
||||||
|
/press-releases/{id}:
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema: { type: integer }
|
||||||
|
get:
|
||||||
|
tags: [press-releases]
|
||||||
|
summary: Pressemitteilung abrufen
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Mitteilung inkl. Firma, Rubrik und Bildern
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data: { $ref: "#/components/schemas/PressRelease" }
|
||||||
|
"403": { description: "Nicht gefunden oder fremdes Konto" }
|
||||||
|
patch:
|
||||||
|
tags: [press-releases]
|
||||||
|
summary: Pressemitteilung ändern (nur draft/rejected)
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: "#/components/schemas/PressReleaseInput" }
|
||||||
|
responses:
|
||||||
|
"200": { description: "Aktualisierte Mitteilung" }
|
||||||
|
"409": { description: "Nur Entwürfe und abgelehnte Mitteilungen sind änderbar" }
|
||||||
|
delete:
|
||||||
|
tags: [press-releases]
|
||||||
|
summary: Pressemitteilung löschen (nicht bei published)
|
||||||
|
responses:
|
||||||
|
"204": { description: "Gelöscht" }
|
||||||
|
"409": { description: "Veröffentlichte Mitteilungen können nicht per API gelöscht werden" }
|
||||||
|
/press-releases/{id}/submit:
|
||||||
|
post:
|
||||||
|
tags: [press-releases]
|
||||||
|
summary: Zur Inhaltsprüfung einreichen
|
||||||
|
description: |
|
||||||
|
Der einzige API-Weg Richtung Veröffentlichung. Durchläuft denselben
|
||||||
|
Funnel wie das Web-Formular (Richtlinien-Filter, Kontingent, Status-Log).
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema: { type: integer }
|
||||||
|
responses:
|
||||||
|
"200": { description: "Eingereicht (Status review)" }
|
||||||
|
"402": { description: "Aktive Buchung erforderlich (Tarif oder Einzel-PM)" }
|
||||||
|
"409": { description: "Nur draft/rejected kann eingereicht werden" }
|
||||||
|
"422": { description: "Kontingent erschöpft oder Richtlinienverstoß" }
|
||||||
|
/press-releases/{id}/images:
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema: { type: integer }
|
||||||
|
get:
|
||||||
|
tags: [images]
|
||||||
|
summary: Bilder einer Mitteilung auflisten
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Bilderliste
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/PressReleaseImage" }
|
||||||
|
post:
|
||||||
|
tags: [images]
|
||||||
|
summary: Bild hochladen (nur bei draft/rejected)
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [image]
|
||||||
|
properties:
|
||||||
|
image:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
description: JPG, PNG oder WebP, max. 8 MB
|
||||||
|
title: { type: string, maxLength: 120 }
|
||||||
|
description: { type: string, maxLength: 500 }
|
||||||
|
copyright: { type: string, maxLength: 255 }
|
||||||
|
is_preview:
|
||||||
|
type: boolean
|
||||||
|
description: Als Vorschaubild setzen (ersetzt ein vorhandenes)
|
||||||
|
responses:
|
||||||
|
"201": { description: "Bild gespeichert" }
|
||||||
|
"409": { description: "Mitteilung ist nicht mehr änderbar" }
|
||||||
|
"422": { description: "Ungültige Datei" }
|
||||||
|
/press-release-images/{imageId}:
|
||||||
|
delete:
|
||||||
|
tags: [images]
|
||||||
|
summary: Bild entfernen (nur bei draft/rejected)
|
||||||
|
parameters:
|
||||||
|
- name: imageId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema: { type: integer }
|
||||||
|
responses:
|
||||||
|
"204": { description: "Gelöscht" }
|
||||||
|
"409": { description: "Mitteilung ist nicht mehr änderbar" }
|
||||||
|
/companies:
|
||||||
|
get:
|
||||||
|
tags: [companies]
|
||||||
|
summary: Eigene Firmenprofile auflisten
|
||||||
|
parameters:
|
||||||
|
- name: per_page
|
||||||
|
in: query
|
||||||
|
schema: { type: integer, maximum: 100 }
|
||||||
|
responses:
|
||||||
|
"200": { description: "Paginierte Liste eigener Firmen" }
|
||||||
|
/companies/{id}:
|
||||||
|
get:
|
||||||
|
tags: [companies]
|
||||||
|
summary: Firmenprofil abrufen
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema: { type: integer }
|
||||||
|
responses:
|
||||||
|
"200": { description: "Firmenprofil" }
|
||||||
|
"403": { description: "Nicht gefunden oder fremdes Konto" }
|
||||||
|
/categories:
|
||||||
|
get:
|
||||||
|
tags: [categories]
|
||||||
|
summary: Aktive Rubriken auflisten
|
||||||
|
responses:
|
||||||
|
"200": { description: "Rubriken inkl. Übersetzungen" }
|
||||||
|
/newsletter/subscribe:
|
||||||
|
post:
|
||||||
|
tags: [newsletter]
|
||||||
|
summary: Newsletter-Anmeldung anlegen (Double-Opt-in folgt per Mail)
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [portal, email]
|
||||||
|
properties:
|
||||||
|
portal:
|
||||||
|
type: string
|
||||||
|
enum: [presseecho, businessportal24]
|
||||||
|
email: { type: string, format: email, maxLength: 190 }
|
||||||
|
salutation_key: { type: string, maxLength: 20 }
|
||||||
|
first_name: { type: string, maxLength: 80 }
|
||||||
|
last_name: { type: string, maxLength: 80 }
|
||||||
|
responses:
|
||||||
|
"201": { description: "Anmeldung angelegt (unbestätigt)" }
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
bearerToken:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
description: "Persönlicher API-Token aus dem Publisher-Konto (Selfservice unter „API-Tokens")."
|
||||||
|
schemas:
|
||||||
|
PressReleaseInput:
|
||||||
|
type: object
|
||||||
|
required: [company_id, category_id, language, title, text]
|
||||||
|
properties:
|
||||||
|
company_id:
|
||||||
|
type: integer
|
||||||
|
description: ID eines eigenen Firmenprofils — bestimmt Newsroom und Portal
|
||||||
|
category_id: { type: integer }
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
minLength: 2
|
||||||
|
maxLength: 2
|
||||||
|
description: de oder en
|
||||||
|
title: { type: string, maxLength: 255 }
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
description: "Erlaubtes HTML: p, br, h2, h3, strong, em, ul, ol, li, blockquote, a"
|
||||||
|
backlink_url: { type: string, format: uri, maxLength: 255 }
|
||||||
|
keywords: { type: string, maxLength: 255 }
|
||||||
|
teaser_begin: { type: integer, minimum: 0 }
|
||||||
|
teaser_end: { type: integer, minimum: 0 }
|
||||||
|
no_export: { type: boolean }
|
||||||
|
PressRelease:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: integer }
|
||||||
|
uuid: { type: string, format: uuid }
|
||||||
|
portal: { type: string }
|
||||||
|
language: { type: string }
|
||||||
|
title: { type: string }
|
||||||
|
slug: { type: string }
|
||||||
|
text: { type: string }
|
||||||
|
backlink_url: { type: [string, "null"] }
|
||||||
|
keywords: { type: [string, "null"] }
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [draft, review, published, rejected, archived]
|
||||||
|
hits: { type: integer }
|
||||||
|
teaser:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
begin: { type: [integer, "null"] }
|
||||||
|
end: { type: [integer, "null"] }
|
||||||
|
no_export: { type: boolean }
|
||||||
|
published_at: { type: [string, "null"], format: date-time }
|
||||||
|
company: { type: object }
|
||||||
|
category: { type: object }
|
||||||
|
images:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/PressReleaseImage" }
|
||||||
|
created_at: { type: string, format: date-time }
|
||||||
|
updated_at: { type: string, format: date-time }
|
||||||
|
PressReleaseImage:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: integer }
|
||||||
|
title: { type: [string, "null"] }
|
||||||
|
description: { type: [string, "null"] }
|
||||||
|
copyright: { type: [string, "null"] }
|
||||||
|
is_preview: { type: boolean }
|
||||||
|
width: { type: integer }
|
||||||
|
height: { type: integer }
|
||||||
|
mime: { type: string }
|
||||||
|
|
@ -283,7 +283,11 @@ Route::get('/robots.txt', function () {
|
||||||
// Sprach-/Edition-unabhängige Routen (API-Doku).
|
// Sprach-/Edition-unabhängige Routen (API-Doku).
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
Route::get('/docs/api/v1', function () {
|
Route::get('/docs/api/v1', function () {
|
||||||
return response((string) file_get_contents(base_path('docs/api/v1.yml')), 200, [
|
$path = public_path('api/v1.yml');
|
||||||
|
|
||||||
|
abort_unless(is_file($path), 404);
|
||||||
|
|
||||||
|
return response((string) file_get_contents($path), 200, [
|
||||||
'Content-Type' => 'application/yaml; charset=UTF-8',
|
'Content-Type' => 'application/yaml; charset=UTF-8',
|
||||||
]);
|
]);
|
||||||
})->name('docs.api.v1');
|
})->name('docs.api.v1');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue