diff --git a/public/api/v1.yml b/public/api/v1.yml new file mode 100644 index 00000000..0e5ffab4 --- /dev/null +++ b/public/api/v1.yml @@ -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 } diff --git a/routes/web.php b/routes/web.php index 3d6d4fba..59bbaf35 100644 --- a/routes/web.php +++ b/routes/web.php @@ -283,7 +283,11 @@ Route::get('/robots.txt', function () { // Sprach-/Edition-unabhängige Routen (API-Doku). // --------------------------------------------------------------------------- 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', ]); })->name('docs.api.v1');