auth: Firmenrechte nur aus nachgewiesener Adresse, 2FA auf allen Login-Wegen (Audit Block 1, Deploy A)
Audit vom 11.09. gegen HEAD geprüft, Entwicklungsplan dazu unter dev/2026-09-11-audit/ENTWICKLUNGSPLAN.md. Dieser Commit ist Deploy A: die sechs kleinen Punkte aus Block 1 ohne Schemaänderung. F01: Der Fortify-Endpunkt PUT /user/profile-information tauschte die E-Mail sofort und setzte die Verifizierung zurück; der Login-Sync vergab danach Firmenrechte allein nach E-Mail-Treffer. Feature abgeschaltet (die Kunden-UI hat den pending_email-Prozess), und der Sync verlangt jetzt ein verifiziertes, aktives Konto – für alle Aufrufer. F02: Google-Login übergibt Konten mit bestätigtem TOTP in die Challenge, wie Passwort- und Magic-Link-Login. F03: showRecoveryCodes ist gesperrt; ein Client-Update konnte die Recovery-Codes entschlüsseln lassen. F08: Nutzer-Detailroute unter can:users:manage (persistente Livewire- Middleware), Gutschriften autorisieren zusätzlich in der Action. F09: Gate::before greift nur für aktive Super-Admins; deaktivierte fallen auf die Gates mit is_active-Prüfung zurück. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
edb2d49199
commit
1d76f5a733
20 changed files with 2229 additions and 69 deletions
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users')->ignore($user->id),
|
||||
],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if ($input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
} else {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
protected function updateVerifiedUser(User $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,20 @@ class GoogleController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
// 2FA aktiv: wie Passwort- und Magic-Link-Login nicht direkt einloggen,
|
||||
// sondern in Fortifys Challenge übergeben (Audit F02). Google belegt
|
||||
// den Besitz des Postfachs, nicht den zweiten Faktor.
|
||||
if ($user->hasEnabledTwoFactorAuthentication()) {
|
||||
$request->session()->put([
|
||||
'login.id' => $user->getKey(),
|
||||
'login.remember' => true,
|
||||
]);
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->route('two-factor.challenge');
|
||||
}
|
||||
|
||||
$request->attributes->set(RecordAuthActivity::AUTH_METHOD_ATTRIBUTE, 'google');
|
||||
Auth::login($user, remember: true);
|
||||
$request->session()->regenerate();
|
||||
|
|
|
|||
|
|
@ -122,7 +122,10 @@ class AppServiceProvider extends ServiceProvider
|
|||
// Permission-Checks (analog zu den Policy-`before()`-Methoden). Damit
|
||||
// greifen feingranulare `can('users:manage')`-Prüfungen auch für
|
||||
// Super-Admins, die keine Spatie-Rolle mit der Permission tragen.
|
||||
Gate::before(fn (User $user, string $ability): ?bool => $user->is_super_admin ? true : null);
|
||||
// Deaktivierung geht vor (Audit F09): ein gesperrter Super-Admin fällt
|
||||
// auf die regulären Gates zurück, die `is_active` prüfen – sonst wäre
|
||||
// die Sperre für bestehende Sessions wirkungslos.
|
||||
Gate::before(fn (User $user, string $ability): ?bool => ($user->is_super_admin && $user->is_active) ? true : null);
|
||||
|
||||
Gate::define('access-admin', fn (User $user): bool => $user->canAccessAdmin());
|
||||
Gate::define('access-customer', fn (User $user): bool => $user->canAccessCustomer());
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ namespace App\Providers;
|
|||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||
use App\Http\Responses\FailedTwoFactorChallengeResponse;
|
||||
use App\Http\Responses\RoleAwareLoginResponse;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
|
|
@ -41,7 +40,6 @@ class FortifyServiceProvider extends ServiceProvider
|
|||
{
|
||||
// Nur die notwendigen Fortify-Aktionen behalten
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class);
|
||||
|
|
|
|||
|
|
@ -101,9 +101,18 @@ class ContactAccessService
|
|||
* (Magic-Link, Passwort, Google) – so greifen auch nachträglich (z. B. per
|
||||
* API) hinzugekommene Firmen/Kontakte mit derselben E-Mail. Rein additiv,
|
||||
* legt kein Konto an und verschickt keine Mail.
|
||||
*
|
||||
* Grundlage ist ausschließlich eine NACHGEWIESENE Adresse (Audit F01): ein
|
||||
* Konto, das seine E-Mail gerade auf eine fremde Firmen-Adresse gewechselt
|
||||
* hat, ist unverifiziert und erhält nichts – der Login-Event feuert auch
|
||||
* vor der Verifizierungs-Weiche. Deaktivierte Konten ebenfalls nicht.
|
||||
*/
|
||||
public function syncMembershipsForEmail(User $user): void
|
||||
{
|
||||
if (! $user->is_active || ! $user->hasVerifiedEmail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = mb_strtolower(trim((string) $user->email));
|
||||
|
||||
if ($email === '') {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,9 @@ return [
|
|||
// Features::registration(), // Deaktiviert, da wir Volt verwenden
|
||||
// Features::resetPasswords(), // Deaktiviert, da wir Volt verwenden
|
||||
// Features::emailVerification(),
|
||||
Features::updateProfileInformation(),
|
||||
// updateProfileInformation() bewusst AUS (Audit F01): der Endpunkt
|
||||
// tauschte die E-Mail sofort und ohne Verifizierung – Firmenrechte
|
||||
// hängen an der Adresse. Die Kunden-UI nutzt den pending_email-Prozess.
|
||||
Features::updatePasswords(),
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
|
|
|
|||
878
dev/2026-09-11-audit/AUDIT.md
Normal file
878
dev/2026-09-11-audit/AUDIT.md
Normal file
|
|
@ -0,0 +1,878 @@
|
|||
# Code-, Projekt- und Entwicklungsaudit
|
||||
|
||||
**Projekt:** pressekonto / presseecho / businessportal24
|
||||
|
||||
**Auditdatum:** 11.09.2026
|
||||
|
||||
**Geprüfter Commit:** `5d0570bee0d94275465b19d6ecb9950cf4d1c1b1`
|
||||
|
||||
**Arbeitsverzeichnis:** `/var/www/html`
|
||||
|
||||
**Auftrag:** Umfangreicher Audit mit Fehleranalyse, Verbesserungsvorschlägen und priorisierten Optimierungen. Keine Änderungen am Anwendungscode.
|
||||
|
||||
**Ergebnis:** 28 priorisierte Befunde, ergänzt um Architekturbeurteilung, Prüfgrenzen, Optimierungsplan und konkrete Regressionstests.
|
||||
|
||||
## 1. Gesamtbeurteilung
|
||||
|
||||
Die Anwendung ist ein gewachsenes, fachlich anspruchsvolles Publishing-System. Sie verbindet öffentliche Nachrichtenportale mit Kunden- und Redaktionsverwaltung, Firmen- und Kontaktzuordnungen, mehreren Anmeldeverfahren, KI-Prüfung, Abonnements, Einmalkäufen, Credits, Legacy-Abrechnung, Newsletter und rechtlich motivierten Änderungsprozessen.
|
||||
|
||||
Die Grundlage ist deutlich weiter als ein unverändertes Starter-Kit: Es gibt zentralisierte Veröffentlichungs- und Abrechnungsservices, gezielte Datenbankindizes, eine ausgelagerte Volltextsuche, Transaktionen und Zeilensperren an wichtigen Geld- und Kontingentoperationen sowie einen großen Bestand an fachlichen Tests. Die jüngere Entwicklung enthält nachvollziehbare Verbesserungen an Sicherheit, Performance und Betrieb.
|
||||
|
||||
**Trotzdem bestehen wesentliche Fehler an den Übergängen zwischen diesen Teilbereichen.** Besonders relevant sind eine unerlaubte Vergabe von Firmenrechten über eine unbestätigte E-Mail-Adresse, unterschiedliche 2FA-Regeln je Anmeldeweg, übergangene Soft Deletes, fehlerhaftes Newsletter-Chunking, eine Abrechnung über ein explizites Vertragsende hinaus und die fehlende Bindung von KI-Ergebnissen an die analysierte Inhaltsversion.
|
||||
|
||||
**Empfehlung:** Die Befunde F01–F07 vor einer weiteren funktionalen Ausweitung bearbeiten. F01 ist innerhalb dieser Gruppe ein Sicherheits-Release-Blocker. Ein umfassender Neuaufbau ist nicht begründet; erforderlich sind gezielte Korrekturen an den bestehenden Sicherheits- und Zustandsgrenzen.
|
||||
|
||||
### 1.1 Wesentliche Ergebnisse auf einen Blick
|
||||
|
||||
| Priorität | Anzahl | Einordnung |
|
||||
|---|---:|---|
|
||||
| P1 | 7 | Hohe Sicherheits- oder Geschäftsauswirkung; vorrangig beheben |
|
||||
| P2 | 19 | Relevante Funktions-, Betriebs-, Performance- oder Qualitätssicherungsfehler |
|
||||
| P3 | 2 | Entwicklungs- und Dokumentationsverbesserungen |
|
||||
|
||||
Es wird kein P0 vergeben: Es wurde kein laufender Angriff oder eingetretener Produktionsschaden nachgewiesen. Die Vergabe P1 bedeutet bei F01 dennoch, dass die beschriebene Rechtevergabe vor dem nächsten regulären Release korrigiert werden sollte. Priorität bewertet die erforderliche Reaktion, nicht die Wahrscheinlichkeit eines bereits erfolgten Vorfalls.
|
||||
|
||||
### 1.2 Belastbarkeit des Ergebnisses
|
||||
|
||||
Der Audit kombiniert Quellcodeanalyse, Gegenprüfung des installierten Framework-Codes, Prüfung bestehender Tests, Git-Historie, Datenbankschema und ausschließlich lesende Datenbankabfragen. Der besonders relevante Authentifizierungsbefund wurde zusätzlich unabhängig anhand der vollständigen Aufrufkette gegengeprüft.
|
||||
|
||||
**Die Anwendungstests, PHPStan und Dependency-Audits konnten nicht ausgeführt werden:** Alle entsprechenden Sail-Aufrufe scheiterten mit `Docker or Podman is not running.` Auch `vendor/bin/sail up -d` scheiterte. Es wurde kein direkter PHP-/Node-Aufruf als Umgehung verwendet.
|
||||
|
||||
Damit sind die Fehler überwiegend **statisch belegt**, jedoch nicht als in dieser Sitzung dynamisch reproduzierte Angriffe oder Integrationstests zu verstehen. F22 ist zusätzlich durch tatsächliche MySQL-Query-Pläne gestützt. Die Datenbankverbindung des Boost-Servers funktionierte unabhängig von der nicht verfügbaren Sail-CLI. Ihre Betriebsrolle wurde nicht als Produktion unterstellt.
|
||||
|
||||
## 2. Prüfauftrag, Teilaufgaben und Vorgehen
|
||||
|
||||
Der Auftrag wurde in die folgenden kleinen Prüfpakete zerlegt und schrittweise bearbeitet. Für den Backend-Teil wurde entsprechend dem Laravel-Skill eine zusätzliche lesende Gegenprüfung eingesetzt.
|
||||
|
||||
| Schritt | Prüfpaket | Bearbeitet | Nachweis / Einschränkung |
|
||||
|---|---|---|---|
|
||||
| 1 | Bestandsaufnahme, Versionen, Umfang, Git-Zustand | Ja | Dateien, Composer-/npm-Konfiguration, Boost application-info, Git |
|
||||
| 2 | Architektur, Domains, Portale, Routen und Zuständigkeiten | Ja | Routing, Provider, Context-Klassen, Services, Dokumentation |
|
||||
| 3 | Anmeldung, Verifizierung, 2FA, Berechtigungen | Ja | Fortify, Socialite, Magic Links, Policies, Middleware, Login-Listener |
|
||||
| 4 | Kunden-/Adminaktionen und Livewire-Zustand | Ja | Kritische öffentliche Actions, Locked-Properties, erneute Autorisierung |
|
||||
| 5 | API-Verträge, Validierung, Eigentums- und Löschgrenzen | Ja | v1, Legacy-Aliasse, Requests, Resources, Scope-Verhalten |
|
||||
| 6 | Veröffentlichung, KI-Prüfung, Quoten, Zeitsteuerung | Ja | Jobs, Veröffentlichungsservice, Zustandsübergänge und Scheduler |
|
||||
| 7 | Geldflüsse und Legacy-Abrechnung | Ja | Wallet, Topups, Rechnungsnummern, MAN-Rechnungen, Käufe, Webhook-nahe Abläufe |
|
||||
| 8 | Newsletter, Hintergrundverarbeitung, Wiederanlauf | Ja | Empfängerauswahl, Start, Versandjob und Fehlergrenzen |
|
||||
| 9 | Datenbank, Suche, Performance und Migration | Ja | Migrations-/Indexprüfung, Query-Pläne, Importpfade |
|
||||
| 10 | Medien, öffentliche Seiten, Cache, SEO und UI | Ja, statisch | Sichtbarkeitsregeln, Uploads, Rendering, Templates; kein visueller Browserlauf |
|
||||
| 11 | Tests, CI, Entwicklungsworkflow und Betriebsdokumentation | Ja | Bestehende Testfälle gelesen, Gates und Runbook geprüft; Laufzeitchecks blockiert |
|
||||
| 12 | Befunde zusammenführen, priorisieren, Maßnahmen und Regressionen | Ja | Dieser Bericht |
|
||||
|
||||
### 2.1 Was nicht verändert oder ausgeführt wurde
|
||||
|
||||
- Kein Anwendungscode, keine Tests, keine Migrationen, keine Konfiguration und keine Abhängigkeiten verändert.
|
||||
- Keine Datenbankänderungen, keine Datenbereinigung, keine Rechnungen erzeugt, keine Newsletter gestartet und keine E-Mails versendet.
|
||||
- Keine Authentifizierungs- oder Berechtigungsangriffe gegen reale Konten ausgeführt.
|
||||
- Keine Deployments, keine Commits und kein Push.
|
||||
- Keine produktiven Nutzdaten, Zugangsdaten oder Tokens in den Bericht übernommen.
|
||||
- Ausschließlich diese angeforderte Markdown-Datei neu angelegt.
|
||||
|
||||
### 2.2 Geltungsbereich und Grenzen
|
||||
|
||||
Die Prüfung deckt alle wesentlichen technischen und fachlichen Bereiche ab. Bei über 116.000 Zeilen in den betrachteten Anwendungs-, View-, Test- und Migrationsdateien ist sie keine Zusicherung einer lückenlosen Zeilenprüfung jeder Datei. Kritische Pfade wurden vertieft untersucht; übrige Bereiche wurden strukturell und anhand von Querschnittssuchen bzw. repräsentativen Implementierungen geprüft.
|
||||
|
||||
Nicht nachgewiesen wurden: vollständige Testgrünheit, reale Browser-/Mobilansichten, tatsächliche Produktionskonfiguration, Infrastruktur-Firewallregeln, Backup-Wiederherstellbarkeit, Produktions-Queueparameter, Lastgrenzen, erfolgreiche echte Stripe-/Google-/SMTP-/KI-Interaktionen oder ein aktueller vollständiger CVE-Status. Ebenso stellt der Bericht keine juristische oder steuerrechtliche Freigabe dar; rechtlich benannte Funktionen werden technisch auf Konsistenz geprüft.
|
||||
|
||||
## 3. Projektbestand und Architektur
|
||||
|
||||
### 3.1 Tatsächlicher Technologiebestand
|
||||
|
||||
Versionen aus Boost `application-info` am Auditdatum, nicht lediglich die Mindestanforderungen aus `composer.json`:
|
||||
|
||||
| Baustein | Installierter Stand |
|
||||
|---|---|
|
||||
| PHP | 8.4 |
|
||||
| Laravel | 12.63.0 |
|
||||
| Fortify | 1.36.2 |
|
||||
| Sanctum | 4.3.1 |
|
||||
| Socialite | 5.27.0 |
|
||||
| Cashier | 16.5.3 |
|
||||
| Livewire / Volt | 4.2.4 / 1.10.5 |
|
||||
| Flux / Flux Pro | jeweils 2.14.0 |
|
||||
| Pest / PHPUnit | 3.8.6 / 11.5.50 |
|
||||
| Larastan / Pint | 3.9.6 / 1.29.1 |
|
||||
| Sail / Boost | 1.57.0 / 2.4.5 |
|
||||
| Alpine / Tailwind CSS | 3.15.0 / 4.1.14 |
|
||||
| Datenbank der verfügbaren Boost-Verbindung | MySQL |
|
||||
|
||||
Ergänzend sind unter anderem Spatie Permission und HTMLPurifier eingebunden. `composer.json` beschreibt das Projekt weiterhin als offizielles Livewire-Starter-Kit. Das ist kosmetisch, bildet die fachliche Eigenständigkeit aber nicht mehr ab.
|
||||
|
||||
### 3.2 Umfang
|
||||
|
||||
| Metrik | Ergebnis | Bedeutung |
|
||||
|---|---:|---|
|
||||
| PHP-Dateien unter `app/` | 363 | Einschließlich Concerns, Services, Commands etc. |
|
||||
| Blade-Dateien unter `resources/views/` | 209 | UI und erhebliche Teile der Volt-Anwendungslogik |
|
||||
| `*Test.php` unter `tests/` | 203 | Dateianzahl, keine Aussage über ausgeführte Tests |
|
||||
| Statisch gezählte `test()`-/`it()`-Deklarationen | 1.481 | Datasets können die tatsächliche Zahl der Ausführungen erhöhen |
|
||||
| Migrationsdateien | 112 | Hohe Schema- und Legacy-Komplexität |
|
||||
| Zeilen in `app`, Views, Tests und Migrationen zusammen | 116.414 | Rohumfang einschließlich Kommentaren/Leerzeilen |
|
||||
| PHPStan-Baseline | 166 Einträge / 188 unterdrückte Vorkommen | Keine neu gemessenen PHPStan-Fehler |
|
||||
|
||||
Besonders große Dateien:
|
||||
|
||||
| Datei | Zeilen | Konsequenz |
|
||||
|---|---:|---|
|
||||
| `resources/views/livewire/customer/press-releases/show.blade.php` | 1.997 | Viele unterschiedliche Käufe, Korrekturen und Anfragen in einem UI-Lebenszyklus |
|
||||
| `app/Services/PressRelease/ProofPdfRenderer.php` | 1.476 | Eigenständige komplexe PDF-Erzeugung; hoher Spezialwissenbedarf |
|
||||
| `resources/views/livewire/admin/press-releases/index.blade.php` | 1.401 | Tabellen-, Filter-, Status- und Verwaltungslogik gebündelt |
|
||||
| `resources/views/livewire/admin/users/edit.blade.php` | 1.186 | Hohe Kopplung von Rollen, Firmen, Kontakten und Profilen |
|
||||
| `resources/views/livewire/customer/bookings.blade.php` | 1.185 | Umfangreicher Bestellzustand und UI in einer Datei |
|
||||
|
||||
Große Dateien sind nicht automatisch fehlerhaft. Hier erschweren sie jedoch die Prüfung von Berechtigungswechseln und Zustandsübergängen. Die Zusammenführung vieler Funktionen sollte nicht mit einer Zusammenführung aller Verantwortlichkeiten verwechselt werden.
|
||||
|
||||
### 3.3 Fachliche Struktur
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Public[Öffentliche Portale: presseecho / businessportal24]
|
||||
Hub[pressekonto: Anmeldung, Kundenbereich, Redaktion]
|
||||
Api[v1-API und Legacy-Service-Aliasse]
|
||||
Context[Portal- und Firmenkontext, Middleware, Policies]
|
||||
Publishing[Veröffentlichungsservice, KI-Prüfung, Medien]
|
||||
Billing[Cashier / Stripe, MAN-Rechnungen, Credits, Einzelkäufe]
|
||||
CRM[Firmen, Kontakte, Mitgliedschaften]
|
||||
Async[Queue, Newsletter, Scheduler, Importe]
|
||||
Data[MySQL, private Dateien, Cache, Suchindex]
|
||||
Public --> Context
|
||||
Hub --> Context
|
||||
Api --> Context
|
||||
Context --> Publishing
|
||||
Context --> Billing
|
||||
Context --> CRM
|
||||
Publishing --> Async
|
||||
Billing --> Async
|
||||
CRM --> Data
|
||||
Publishing --> Data
|
||||
Billing --> Data
|
||||
Async --> Data
|
||||
```
|
||||
|
||||
Es bestehen zwei voneinander unabhängige Zugriffsachsen: **Portal/Sprache** für die Veröffentlichung und **Benutzer/Firmenzuordnung** für die Verwaltung. Dazu kommt der Lebenszyklus eines Datensatzes: aktiv, archiviert, mit 410 entfernt oder soft-gelöscht. Mehrere Befunde entstehen dadurch, dass eine Abfrage nur eine dieser Achsen berücksichtigt.
|
||||
|
||||
### 3.4 Bereits gut gelöst
|
||||
|
||||
- Veröffentlichungen laufen zentral über `PressReleaseService`; Publikation prüft unter Sperren erneut Zustand, Quoten und verfügbare Käufe.
|
||||
- Walletsaldo und Ledger werden transaktional geschrieben. Topup-Gutschriften prüfen `credited_at` unter Row Lock.
|
||||
- Rechnungsnummern beruhen auf einer eindeutigen Sequenz und Zeilensperren.
|
||||
- Boost-Inventar verwendet stabile Sperrzeilen und eine definierte Sperrreihenfolge.
|
||||
- Die API besitzt Tokenfähigkeiten, aktive Benutzerprüfung und ein tokenbezogenes Rate Limit. Fremde Autorschaft wird in den untersuchten PM-Pfaden geprüft.
|
||||
- Magic-Login-Tokens werden gehasht gespeichert und beim Verbrauch atomar beansprucht. Der Magic-Login respektiert bereits die lokale 2FA.
|
||||
- Eigene Admin-/Customer-Middleware ist für Livewire-Folgeanfragen registriert. Viele Actions autorisieren zusätzlich explizit.
|
||||
- Presse-HTML wird über einen zentralen Sanitizer ausgegeben; strukturierte Daten verwenden `JSON_HEX_TAG`. Unescaped-Ausgabe ist deshalb nicht pauschal als XSS eingestuft worden.
|
||||
- Medien liegen konzeptionell auf privaten Disks und werden über Controller ausgeliefert. Das ist die richtige Grundlage, auch wenn F04 eine Lücke in der Sichtbarkeitsprüfung zeigt.
|
||||
- Zahlreiche Scheduler-Tasks besitzen `withoutOverlapping()` und Failure-Hooks; es existiert ein Betriebs-Runbook.
|
||||
- Die Suchindex-Auslagerung und zahlreiche zusammengesetzte Indizes zeigen eine bewusste Auseinandersetzung mit großen Beständen.
|
||||
- Tests decken viele sensible Themen ausdrücklich ab: Rechteeskalation, Verifizierung, Billing, Scheduling, Compliance, Legacy-Kompatibilität und Performanceverhalten.
|
||||
|
||||
## 4. Befundregister
|
||||
|
||||
**Nachweisarten:** **S** = statisch anhand konkreter Aufrufkette belegt; **D** = durch lesende Datenbankprüfung ergänzt; **B** = Auswirkung hängt von einer ausdrücklich genannten Betriebs-/Parallelitätsbedingung ab. Ein B-Befund ist kein nachgewiesener Produktionsvorfall.
|
||||
|
||||
| ID | Priorität | Nachweis | Befund |
|
||||
|---|---|---|---|
|
||||
| F01 | P1 | S | Unbestätigte E-Mail kann dauerhafte fremde Firmenrechte auslösen |
|
||||
| F02 | P1 | S | Google-Login umgeht lokale Zwei-Faktor-Authentifizierung |
|
||||
| F03 | P1 | S | Sicherheits-UI umgeht erneute Identitätsprüfung; Recovery-Anzeige manipulierbar |
|
||||
| F04 | P1 | S | Soft Deletes werden in API, Vorschau, Kundenlisten und Medien übergangen |
|
||||
| F05 | P1 | S/D | Newsletter-Chunking dupliziert und überspringt Empfänger |
|
||||
| F06 | P1 | S | MAN-Abrechnung ignoriert explizites Legacy-Vertragsende |
|
||||
| F07 | P1 | S/B | KI-Ergebnisse sind nicht an die analysierte Inhaltsversion gebunden |
|
||||
| F08 | P2 | S/B | Admin-Gutschriften autorisieren nur beim Öffnen der Komponente |
|
||||
| F09 | P2 | S/B | Globaler Superadmin-Kurzschluss umgeht Kontodeaktivierung |
|
||||
| F10 | P2 | S | Automatisch vergebene Firmenrechte werden bei Wegfall der Grundlage nicht entzogen |
|
||||
| F11 | P2 | S | API-Pagination und Eingabeverträge unvollständig validiert |
|
||||
| F12 | P2 | S/D | Meldeverfahren ordnen URLs nur anhand des Slugs zu |
|
||||
| F13 | P2 | S/B | Firmenlöschung/-deaktivierung invalidiert öffentliche Seitencaches nicht zuverlässig |
|
||||
| F14 | P2 | S/B | Newsletter-Start besitzt keinen sicheren Wiederanlauf |
|
||||
| F15 | P2 | S/B | Newsletter-Job kann doppelt senden und Zähler inkonsistent hinterlassen |
|
||||
| F16 | P2 | S | Nicht abrechenbare Altverträge können den Rechnungslauf blockieren |
|
||||
| F17 | P2 | S/B | KI-Retrybudget und Worker-Timeout passen nicht zusammen |
|
||||
| F18 | P2 | S/B | Monatskontingent-Reset ist nicht periodengebunden und nicht idempotent |
|
||||
| F19 | P2 | S/B | Gleichzeitige Aufrufaggregatoren zählen Ereignisse mehrfach |
|
||||
| F20 | P2 | S | Scheduler-Publikationsfehler liefern trotzdem Exit-Code Erfolg |
|
||||
| F21 | P2 | S/B | Legacy-Import lädt große Gesamttabellen und hinterlässt Teilimporte |
|
||||
| F22 | P2 | S/D | `LOWER(email)` verhindert den vorhandenen E-Mail-Index |
|
||||
| F23 | P2 | S/B | Dateifehler können scheinbar erfolgreiche Uploads und verlorene Logos verursachen |
|
||||
| F24 | P2 | S | Lint-CI formatiert, erzwingt aber keine saubere Formatierung im Commit |
|
||||
| F25 | P2 | S | Test-/Analysekonfiguration lässt wesentliche Produktionspfade unbewiesen |
|
||||
| F26 | P2 | S | Eigenes Anmeldemodal besitzt keine explizite Fokusführung |
|
||||
| F27 | P3 | S | Standard-Entwicklungsstart startet keinen Vite-Server |
|
||||
| F28 | P3 | S | Einstiegspunkte der Dokumentation zeigen widersprüchliche Entwicklungsstände |
|
||||
|
||||
## 5. Sicherheits- und Zugriffsprüfung
|
||||
|
||||
### F01 — Fremde Firmenrechte über eine unbestätigte E-Mail-Adresse
|
||||
|
||||
**Priorität:** P1, innerhalb dieses Audits Sicherheits-Release-Blocker.
|
||||
|
||||
**Belastbarkeit:** Vollständige statische Aufrufkette, unabhängig gegengeprüft; nicht gegen reale Konten ausgeführt.
|
||||
|
||||
**Fundstellen:**
|
||||
|
||||
- `config/fortify.php:150`: Profiländerung ist als Fortify-Feature aktiv.
|
||||
- `app/Providers/FortifyServiceProvider.php:44`: eigene Profil-Action ist gebunden.
|
||||
- `vendor/laravel/fortify/routes/routes.php:100–103`: `PUT /user/profile-information` benötigt Authentifizierung, keine verifizierte E-Mail.
|
||||
- `app/Actions/Fortify/UpdateUserProfileInformation.php:48–56`: E-Mail wird sofort ersetzt, `email_verified_at` auf `null` gesetzt.
|
||||
- `resources/views/livewire/auth/login.blade.php:62–69`: Login erfolgt vor der Verifizierungsprüfung.
|
||||
- `app/Listeners/SyncCompanyMembershipsOnLogin.php:22–28` und `app/Services/Auth/ContactAccessService.php:104–119`: E-Mail-basierte Zuordnung ohne Verifizierungsprüfung.
|
||||
- `app/Services/Auth/ContactAccessService.php:237–246`: Vergabe von `responsible` und additive Kontaktzuordnung.
|
||||
- `app/Policies/CompanyPolicy.php:34–40,64–74`: diese Rolle gewährt Schreibzugriff.
|
||||
|
||||
**Fehlerszenario:** Ein Benutzer kontrolliert ein Passwortkonto und eine eigene E-Mail-Adresse. Eine fremde E-Mail steht in `companies.email` oder an einem zugeordneten Pressekontakt, gehört aber noch keinem anderen `users.email`-Datensatz. Der Benutzer ändert über den weiterhin aktiven Fortify-Endpunkt seine eigene Kontoadresse auf diese fremde Adresse. Nach Ab- und Anmeldung mit seinem unveränderten Passwort feuert der Login-Listener und trägt ihn bereits als Firmenverantwortlichen ein. Anschließend kann er über denselben Endpunkt auf seine eigene Adresse zurückwechseln und diese bestätigen. Die zuvor vergebenen Firmenmitgliedschaften bleiben erhalten.
|
||||
|
||||
**Auswirkung:** Dauerhafter Zugriff auf fremde Firmenstammdaten, Kontakte und entsprechend berechtigte Pressemitteilungen, ohne Besitznachweis des fremden Postfachs. Die `verified`-Middleware verhindert nur den unmittelbaren Zugang während des unverifizierten Zwischenzustands; sie verhindert nicht die Vergabe der Berechtigung.
|
||||
|
||||
**Abgrenzung:** Bestehende `users.email`-Einträge begrenzen durch Unique-Validierung die angreifbaren Zieladressen. CSRF ist keine Abwehr gegen einen Benutzer, der reguläre Änderungen am eigenen Konto ausführt. Ein fremdes Benutzerkonto wird durch diese Kette nicht unmittelbar übernommen; betroffen sind Firmenrechte.
|
||||
|
||||
**Empfehlung:** E-Mail-basierte Berechtigungen ausschließlich aufgrund einer aktuell nachgewiesenen Kontoadresse vergeben. Den Fortify-Endpunkt auf denselben `pending_email`-Prozess wie die reguläre Sicherheits-UI führen oder deaktivieren, wenn er nicht benötigt wird. Die Kontrolle muss im Zuordnungsservice liegen und alle Aufrufer erfassen. Bestehende automatisch entstandene Zuordnungen anschließend gezielt auf ihre Verifikationsgrundlage untersuchen; nicht pauschal alle Mitgliedschaften löschen.
|
||||
|
||||
**Erforderlicher Regressionstest:** Eigene bestätigte Adresse → fremde, nur am Kontakt gespeicherte Adresse → Passwortlogin → Rückwechsel und Bestätigung eigener Adresse. Während der gesamten Sequenz dürfen weder `company_user` noch `contact_user` unberechtigte Zuordnungen erhalten. Zusätzlich den direkten Serviceaufruf für unverifizierte und deaktivierte Benutzer prüfen.
|
||||
|
||||
### F02 — Google-Login überspringt lokale 2FA
|
||||
|
||||
**Priorität:** P1.
|
||||
|
||||
**Fundstellen:** `app/Http/Controllers/Auth/GoogleController.php:37–57`; als Gegenbeispiel `app/Http/Controllers/Auth/MagicLinkConsumeController.php:68–77` und `resources/views/livewire/auth/login.blade.php:49–59`.
|
||||
|
||||
Nach erfolgreicher Google-Auflösung ruft der Callback unmittelbar `Auth::login($user, remember: true)` auf. Eine Prüfung auf `hasEnabledTwoFactorAuthentication()` und die Übergabe in Fortifys Challenge fehlen. Passwort- und Magic-Login prüfen dies bereits.
|
||||
|
||||
**Trigger:** Ein aktives Bestandskonto mit bestätigter lokaler 2FA meldet sich über Google mit der passenden Adresse an. Die Anwendung erstellt sofort eine Sitzung. Dass der Google-Account eventuell selbst MFA verwendet, beweist nicht die Erfüllung der lokal konfigurierten TOTP-Anforderung.
|
||||
|
||||
**Auswirkung:** Die Sicherheitszusage „2FA aktiviert“ hängt vom gewählten Anmeldeweg ab. Das betrifft auch privilegierte Konten, sofern Google-Login für ihre Adresse funktioniert.
|
||||
|
||||
**Empfehlung:** Gemeinsame Login-Abschlusslogik für Passwort, Magic Link und Google: aktive Identität prüfen, gegebenenfalls Challengezustand setzen und erst nach erfolgreicher Challenge authentifizieren. Die Entscheidung über dauerhafte Anmeldung ebenfalls zentralisieren.
|
||||
|
||||
**Regression:** Google-Callback mit aktiviertem lokalen TOTP muss als Gast enden und zur Challenge führen. Ohne bestätigte 2FA bleibt das bisherige Verhalten. Deaktivierte Konten müssen gesperrt bleiben. `tests/Feature/Auth/GoogleLoginTest.php` deckt derzeit Neuanlage, Zuordnung, Deaktivierung und Fehler ab, jedoch diesen 2FA-Fall nicht.
|
||||
|
||||
### F03 — Sicherheitsaktionen ohne erneute Identitätsprüfung; Recovery-Anzeige clientseitig freischaltbar
|
||||
|
||||
**Priorität:** P1.
|
||||
|
||||
**Fundstellen:** `resources/views/livewire/customer/security.blade.php:39,74–109,120–164,179–186`; `routes/customer.php:28–31,73`; `config/fortify.php:152–155`; `vendor/laravel/fortify/routes/routes.php:142–172`.
|
||||
|
||||
Die Konfiguration verlangt `confirmPassword => true`. Fortifys eigene 2FA-Routen setzen dafür `password.confirm` ein. Die Volt-Komponente ruft die Fortify-Actions direkt auf, ohne diesen Schutz gleichwertig zu prüfen. Deaktivierung der 2FA, Erzeugung neuer Recovery-Codes und Start einer E-Mail-Änderung sind allein mit einer bestehenden Sitzung möglich.
|
||||
|
||||
Zusätzlich hängt die Anzeige entschlüsselter Recovery-Codes von der ungesperrten öffentlichen Property `showRecoveryCodes` ab. Diese Property ist vom Client veränderbar. „Nur einmal anzeigen“ ist dadurch lediglich das normale UI-Verhalten, keine serverseitige Sicherheitsgrenze.
|
||||
|
||||
**Trigger:** Eine bereits bestehende oder entwendete Sitzung ruft `disableTwoFactorAuthentication()` bzw. `regenerateRecoveryCodes()` auf oder setzt `showRecoveryCodes` auf `true`. Dafür ist keine erneute Kenntnis des Passworts oder eines anderen starken Nachweises erforderlich.
|
||||
|
||||
**Auswirkung:** Temporärer Sitzungszugriff kann zu dauerhaft geschwächter Kontosicherheit führen. Der Angreifer braucht eine Sitzung; es handelt sich nicht um einen anonymen Zugriff auf fremde Recovery-Codes.
|
||||
|
||||
**Empfehlung:** Einen zeitlich begrenzten, serverseitig geprüften erneuten Identitätsnachweis vor sicherheitskritischen Aktionen und vor Recovery-Ausgabe verlangen. Für passwortlose Konten einen ausdrücklich definierten sicheren Ersatz vorsehen. `showRecoveryCodes` mindestens gegen Clientmutation schützen; das ersetzt die erneute Identitätsprüfung nicht. Sicherheitsänderungen nachvollziehbar protokollieren und den Kontoinhaber benachrichtigen.
|
||||
|
||||
**Regression:** Ohne aktuellen Nachweis müssen direkte Action-Aufrufe und `set('showRecoveryCodes', true)` scheitern. Die Tests in `tests/Feature/CustomerProfileSecurityTest.php:209–270` prüfen zwar Einrichtung und normale Sichtbarkeit, erlauben die Regeneration bisher ohne erneuten Nachweis.
|
||||
|
||||
### F04 — Soft Deletes werden als Nebenwirkung der Portal-Entkopplung entfernt
|
||||
|
||||
**Priorität:** P1.
|
||||
|
||||
**Fundstellen:**
|
||||
|
||||
- `app/Http/Controllers/Api/V1/PressReleaseController.php:36,238,257`.
|
||||
- `app/Http/Controllers/Api/V1/CompanyController.php:17,34`.
|
||||
- `app/Http/Controllers/PressReleasePreviewController.php:39–49`.
|
||||
- `app/Http/Controllers/Web/PressReleaseImageMediaController.php:23–26` und entsprechender Attachment-Controller.
|
||||
- `app/Http/Controllers/Web/Concerns/AuthorizesPressReleaseMediaAccess.php:39–46`.
|
||||
- `app/Models/PressRelease.php:537–542`.
|
||||
- `resources/views/livewire/customer/press-kits/show.blade.php:233–260`.
|
||||
|
||||
`withoutGlobalScopes()` deaktiviert nicht nur den Portalfilter, sondern auch den Soft-Delete-Filter. Nachfolgende `ownedByApiUser()`-/`accessibleBy()`-Scopes stellen den Soft-Delete-Schutz nicht wieder her. Die betroffenen Modelle verwenden tatsächlich `SoftDeletes`.
|
||||
|
||||
**Konkrete Folgen:**
|
||||
|
||||
1. Eine über die API gelöschte eigene Entwurfs-PM kann weiterhin über Index/Show gefunden werden; auch das Update sucht ohne Soft-Delete-Scope.
|
||||
2. Ein bestehender Vorschau-Token kann eine soft-gelöschte PM weiter laden.
|
||||
3. Gelöschte Kontakte erscheinen weiterhin in der Firmenansicht, weil die Kontaktliste und der Einzelkontaktlookup alle Scopes entfernen.
|
||||
4. Medien laden die zugehörige Firma ohne Scopes. Eine soft-gelöschte, zuvor aktive Firma bleibt auf dem geladenen Model `is_active=true`; eine veröffentlichte PM kann dadurch weiterhin als öffentlich gelten. Die Instanzprüfung berücksichtigt `deleted_at` nicht. Die normale Firmenlöschung in `resources/views/livewire/admin/companies/edit.blade.php:262` ändert diesen Aktivwert nicht.
|
||||
|
||||
**Wichtiger Testbefund:** `tests/Feature/PressReleasePreviewTest.php:237–247` heißt „preview link for soft deleted press release returns 404“, führt tatsächlich aber `$pr->forceDelete()` aus. Der Test beweist nur den Fall eines physisch entfernten Datensatzes und verdeckt genau diese Lücke.
|
||||
|
||||
**Empfehlung:** Portalübergreifende Abfragen mit `withoutGlobalScope(PortalScope::class)` formulieren. Gelöschte Datensätze nur in ausdrücklich dafür vorgesehenen Verwaltungsfunktionen über `withTrashed()` zulassen. Sichtbarkeitsregeln für PM und Firma einschließlich Soft Delete zentralisieren und sowohl in Query- als auch Instanzprüfungen gleich umsetzen.
|
||||
|
||||
**Regression:** Soft Delete separat für PM, Firma, Kontakt, Bild und Anhang testen. Bestehende Vorschau- und Medienlinks sowie API-GET/PATCH nach Löschung prüfen. Außerdem verifizieren, dass berechtigte portalübergreifende Zugriffe auf nicht gelöschte Daten erhalten bleiben.
|
||||
|
||||
### F08 — Admin-Gutschriften prüfen Rechte nur in `mount()`
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `resources/views/livewire/admin/users/show.blade.php:25–27,39–82`; `app/Services/Billing/AdminGrantService.php:29–49,59–99`; `routes/admin.php`, Benutzer-Detailroute.
|
||||
|
||||
`mount()` verlangt `users:manage`; `grantCredits()` und `grantFreePressReleases()` prüfen diese Berechtigung nicht erneut. Auch der Service prüft den übergebenen Ausführenden nicht. Die persistente Admin-Middleware kontrolliert lediglich den Adminbereichszugang.
|
||||
|
||||
**Trigger:** Ein berechtigter Admin öffnet die Seite. Danach wird ihm `users:manage` entzogen oder er wird zum Editor herabgestuft. Als Editor erfüllt er weiterhin `access-admin` und kann aus dem alten Livewire-Zustand Gutschriften auslösen. Das gesperrte `$id` verhindert die Mutation des Zielbenutzers, aber nicht diesen Rechteentzugfall.
|
||||
|
||||
**Empfehlung:** Jede finanzwirksame Action erneut autorisieren; alternativ bzw. ergänzend den Aufruf zentral im Service absichern. Berechtigungen auf der Route als persistente Grenze setzen, wenn das gesamte Modul diese benötigt.
|
||||
|
||||
**Regression:** Komponente berechtigt öffnen, Rolle/Berechtigung während ihrer Lebensdauer entziehen, dann beide Actions aufrufen. Weder Ledger noch Käufe dürfen verändert werden. Der vorhandene Test `AdminGrantTest` prüft nur den initial gesperrten Seitenaufruf.
|
||||
|
||||
### F09 — Superadmin-Kurzschluss ignoriert deaktivierte Konten
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Providers/AppServiceProvider.php:114–119`; `app/Models/User.php:537–558`; `app/Http/Middleware/EnsureUserIsAdmin.php:21–25` und `EnsureUserIsCustomer.php`.
|
||||
|
||||
`Gate::before` liefert für jeden Superadmin `true`, bevor die Access-Gates überhaupt `canAccessAdmin()` oder `canAccessCustomer()` ausführen. Diese Methoden prüfen eigentlich zuerst `is_active`.
|
||||
|
||||
**Trigger:** Ein bereits angemeldetes Superadmin-Konto wird deaktiviert. Bei nachfolgenden Requests mit bestehender Sitzung kann der Gate-Kurzschluss die Sperre umgehen. Die gesonderten Login-Prüfungen schützen einen neuen Login, ersetzen aber nicht die Prüfung bereits bestehender Sitzungen.
|
||||
|
||||
**Empfehlung:** Kontodeaktivierung als vorrangige, nicht übersteuerbare Grenze behandeln. Superadmin-Erleichterungen erst danach anwenden. Bestehende Sitzungen und Tokens im Sperrprozess nach definierter Regel widerrufen.
|
||||
|
||||
**Regression:** Aktiven Superadmin anmelden, deaktivieren, Admin-/Customer-Route und Livewire-Folgeaktion aufrufen. Access-Gate und Model-Methode müssen denselben Ablehnungswert liefern.
|
||||
|
||||
### F10 — Automatisch verliehene Firmenrechte besitzen keinen Entzugslebenszyklus
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Services/Auth/ContactAccessService.php:104–119,225–246`; `resources/views/livewire/customer/press-kits/show.blade.php:193–209`; `app/Policies/CompanyPolicy.php:64–74`.
|
||||
|
||||
Die E-Mail-Synchronisation ist ausdrücklich rein additiv. Wird eine Kontaktadresse geändert, ein Pressekontakt gelöscht oder eine Firmenadresse ersetzt, bleibt ein zuvor automatisch vergebener `responsible`-Pivot bestehen. Die Policy benötigt später nur diesen Pivot und nicht mehr die ursprüngliche E-Mail-Übereinstimmung.
|
||||
|
||||
**Trigger:** Ein ehemaliger Ansprechpartner erhält zunächst legitimen Zugang, wird anschließend aus den Firmenkontakten entfernt und behält trotzdem Schreibrechte. Dies ist unabhängig vom missbräuchlichen Verifikationspfad F01.
|
||||
|
||||
**Empfehlung:** Herkunft automatisch verliehener Rechte speichern und einen eigenen Entzugsprozess implementieren. Manuell erteilte Team-/Eigentümerrechte dürfen dabei nicht pauschal mitgelöscht werden. Im Administrationsprozess sichtbar machen, dass Kontaktlöschung und Berechtigungsentzug aktuell verschiedene Vorgänge sind.
|
||||
|
||||
**Regression:** Automatische Zuordnung erzeugen, die letzte berechtigende E-Mail-Grundlage entfernen, anschließend Firmenzugriff prüfen. Manuell zusätzlich erteilte Rechte müssen nach der fachlich festgelegten Regel erhalten bleiben.
|
||||
|
||||
## 6. Veröffentlichung, API, Medien und öffentliche Auslieferung
|
||||
|
||||
### F07 — KI-Bewertung kann auf einer veralteten Inhaltsversion beruhen
|
||||
|
||||
**Priorität:** P1.
|
||||
|
||||
**Fundstellen:** `app/Jobs/ClassifyPressRelease.php:44–81`; `app/Jobs/ScorePressRelease.php:38–61`; `app/Services/PressRelease/PressReleaseService.php:123–160,177–189,214–240`.
|
||||
|
||||
Die Jobs kennen die PM-ID, jedoch keine Inhaltsversion. Sie laden den Inhalt, warten auf die externe Bewertung und speichern danach das Ergebnis ohne Vergleich mit dem inzwischen aktuellen Inhalt. Die Veröffentlichung lädt und sperrt zwar das aktuelle Model, prüft aber nicht, ob dieses den tatsächlich bewerteten Text enthält.
|
||||
|
||||
**Trigger:** Während Job A Version V1 bewertet, wird V2 gespeichert. Danach kommt A zurück und übernimmt sein Ergebnis für V2; ein jüngerer Job kann zusätzlich durch eine ältere Antwort überschrieben werden. Bei der Erstprüfung sind `classification`/`content_score` noch `null`; die Recheck-Helfer planen in diesem Zustand keinen Ersatzlauf ein. Administrative Änderungen und erlaubte Review-Bearbeitung bieten entsprechende Übergänge.
|
||||
|
||||
**Auswirkung:** Veröffentlichter Inhalt, Bewertung und KI-Audit können auseinanderfallen. Die erneute Blacklistprüfung im Veröffentlichungsservice kontrolliert eine andere, engere Regel und ersetzt keine Inhaltsversionsbindung.
|
||||
|
||||
**Empfehlung:** Monotone Inhaltsversion oder Hash bei Einplanung und Analyse speichern. Ergebnisübernahme und daraus abgeleitete Publikation nur bei übereinstimmender aktueller Version erlauben. Veraltete Antworten nachvollziehbar verwerfen und gegebenenfalls eine aktuelle Prüfung einplanen.
|
||||
|
||||
**Regression:** Fake-Treiber verändert den Text während der Analyse; zwei Jobs beenden sich in umgekehrter Reihenfolge; erste Analyse läuft parallel zu einer Bearbeitung. Ein altes Ergebnis darf weder neue Werte überschreiben noch V2 freigeben.
|
||||
|
||||
### F11 — API-Eingaben können Pagination und fachliche Verträge umgehen
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Http/Controllers/Api/V1/PressReleaseController.php:36–41`; `app/Http/Controllers/Api/V1/CompanyController.php:17–20`; `app/Http/Requests/Api/V1/StorePressReleaseRequest.php:33–46`; entsprechender Update-Request.
|
||||
|
||||
`paginate(min((int) per_page, 100))` begrenzt nur nach oben. Bei `per_page=-1` bleibt ein negativer Wert übrig. Der installierte Eloquent-Builder reicht diesen an `forPage()` weiter; `Query\Builder::limit()` setzt für negative Werte kein Limit (`vendor/.../Eloquent/Builder.php:1116–1126`, `vendor/.../Query/Builder.php:3110–3118`). Die Abfrage kann dadurch den vollständigen für den Benutzer sichtbaren Bestand einschließlich Relationen laden.
|
||||
|
||||
Weitere Vertragslücken: `language` verlangt lediglich zwei Zeichen statt der unterstützten Ausgaben `de|en`; `category_id` verlangt Existenz, aber keine aktive, zum Zielportal passende Kategorie. Der Statusfilter wird ungeprüft an einen auf `string` typisierten Callback übergeben; ein Arrayparameter kann statt einer kontrollierten 422-Antwort einen Typfehler auslösen.
|
||||
|
||||
**Auswirkung:** Unbegrenzte Ergebnismengen für große Konten, unnötige Speicher-/Datenbanklast und fachlich ungültige Datensätze. Dies ist keine nachgewiesene fremde Datenfreigabe: die Eigentumsbedingungen bleiben bestehen.
|
||||
|
||||
**Empfehlung:** Queryparameter explizit als Integer im Bereich 1–100, Status als erlaubten Enumwert und Sprache als unterstützte Ausgabe validieren. Kategorie und Portal zusammen validieren; Legacy-Verträge bewusst separat berücksichtigen. Maximale Textgröße definieren, wenn API- und Webeingaben dieselben fachlichen Grenzen haben sollen.
|
||||
|
||||
**Regression:** `per_page` mit -1, 0, 1, 100, 101, Text und Array; `status[]`; `language=zz`; inaktive bzw. portalunpassende Kategorie. Ungültige Eingaben kontrolliert ablehnen, SQL-Limit stets begrenzen.
|
||||
|
||||
### F12 — Melde-URL wird ohne Portal und Sprache einer PM zugeordnet
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Http/Controllers/Web/PressReleaseReportController.php:325–335`; `app/Http/Controllers/Web/ReportFormController.php:112–124`; `app/Models/PressRelease.php:43–45` und Schemaindex `press_releases_portal_language_slug_unique`.
|
||||
|
||||
Beide Controller extrahieren lediglich den Slug hinter `/press-release/`, entfernen den Portal-Scope und wählen die erste veröffentlichte PM. Host und Ausgabe der gemeldeten URL werden nicht berücksichtigt. Laut tatsächlich vorhandenem Unique-Index ist ein Slug nur innerhalb von Portal und Sprache eindeutig.
|
||||
|
||||
**Trigger:** Zwei Portale oder Ausgaben besitzen denselben Slug. Eine Meldung zur zweiten URL kann den zuerst gefundenen Datensatz der anderen Ausgabe treffen. Auch eine fremde Domain mit passendem Pfad wird nicht als fremd erkannt.
|
||||
|
||||
**Auswirkung:** Falsche Zuordnung im Bearbeitungsprozess und gegebenenfalls Versand des Verifikationslinks an den falschen hinterlegten Pressekontakt. Es wird nicht an eine beliebig eingegebene Angreiferadresse versendet; diese vorhandene Schutzmaßnahme bleibt wirksam.
|
||||
|
||||
**Empfehlung:** Gemeinsamen Resolver für eigene kanonische und ausdrücklich unterstützte Legacy-URLs verwenden: Host-Whitelist, Portal, Sprache und Slug zusammen auflösen; öffentliche Sichtbarkeit einheitlich berücksichtigen. Nicht zuordenbare Meldungen im allgemeinen Meldeweg bewusst ohne PM-Bezug behandeln.
|
||||
|
||||
**Regression:** Gleicher Slug in beiden Portalen und in `de/en`, URL einer fremden Domain, kodierter Slug, gelöschte bzw. künftig terminierte PM. Jeweils genau die beabsichtigte Zuordnung oder einen kontrollierten Nichttreffer erwarten.
|
||||
|
||||
### F13 — Öffentliche Caches überleben normale Firmenlöschung/-deaktivierung
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `resources/views/livewire/admin/companies/edit.blade.php:121–186,250–265`; `app/Observers/AdminPerformanceCacheObserver.php:17–35`; `app/Services/Web/NewsroomDetailPageData.php:26–57,65–74`; `app/Support/WebPageCache.php:25–33,136–138`.
|
||||
|
||||
Der Firmenobserver invalidiert Admin-Statistiken. Die normale Firmenlöschung ruft lediglich `delete()` auf. Der öffentliche Newsroom prüft vor dem Seiten-Cache nur den Sonderfall `gone_at`, nicht die normale Löschung bzw. Deaktivierung. Beim Cachetreffer bleibt der vorher gespeicherte Inhalt verfügbar.
|
||||
|
||||
**Trigger:** Newsroomseite wird gecacht; danach wird die Firma normal gelöscht oder deaktiviert. Der nächste Aufruf erhält zunächst die gespeicherten Daten. Das öffentliche Cachesystem erlaubt ein Stale-Fenster bis 3.600 Sekunden. Das tatsächliche Ende hängt vom Neuaufbau und dessen Fehlerbehandlung ab; eine exakte maximale Sichtbarkeitsdauer wurde nicht gemessen.
|
||||
|
||||
**Abgrenzung:** Der spezielle 410-Entfernungsprozess besitzt bereits einen bewussten öffentlichen Cache-Flush. Gerade deshalb darf dieses Verhalten nicht auf die übrigen Löschwege übertragen werden.
|
||||
|
||||
**Empfehlung:** Alle öffentlichen Sichtbarkeitswechsel an denselben Cache-/Entfernungsprozess anschließen. Bei sensiblen Entfernungen den aktuellen Sichtbarkeitsstatus vor jedem Cachetreffer prüfen. Seiten-Cache und Browser-/CDN-Mediencache getrennt behandeln; ein Anwendungsflush löscht keine bereits extern gecachten Bilder.
|
||||
|
||||
**Regression:** Cache aufbauen, Firma löschen/deaktivieren, unmittelbar danach Newsroom, Kategorie, Startseite und Medien abrufen. Auch Kontaktänderungen mit gecachter Newsroomansicht prüfen.
|
||||
|
||||
### F23 — Dateisystemfehler können Erfolg vortäuschen oder bestehende Logos verlieren
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Services/PressRelease/PressReleaseAttachmentStorage.php:59–80`; `config/filesystems.php:33–47`; `resources/views/livewire/customer/press-kits/show.blade.php:115–130`; entsprechender Austausch in `resources/views/livewire/admin/companies/edit.blade.php:150–165`.
|
||||
|
||||
Der Attachment-Service ignoriert den Rückgabewert von `$disk->put()`. Gleichzeitig konfigurieren die Disks `throw => false` und `report => false`. Ein fehlgeschlagener Schreibvorgang kann somit normale Dateimetadaten zurückliefern, obwohl die Datei nicht gespeichert wurde.
|
||||
|
||||
Beim Logo-Austausch wird das bisherige Logo einschließlich Varianten vor der erfolgreichen Speicherung des neuen Logos gelöscht. Scheitert die neue Speicherung oder die anschließende Datenbankänderung, kann das alte Logo bereits unwiederbringlich fehlen.
|
||||
|
||||
**Trigger:** Voller Datenträger, Schreibrechteproblem oder fehlgeschlagener Storagezugriff beim Upload/Austausch.
|
||||
|
||||
**Empfehlung:** Storagefehler durch geprüften Rückgabewert bzw. geeignete Exceptionkonfiguration eindeutig behandeln. Neue Datei zuerst vollständig speichern, dann DB-Referenz wechseln und alte Datei erst anschließend entfernen. Neue Dateien bei gescheitertem DB-Commit aufräumen. Eine DB-Transaktion allein macht Dateisystemoperationen nicht atomar.
|
||||
|
||||
**Regression:** Storage-Fake/Mock liefert `false`; neue Datei kann nicht geschrieben werden; DB-Save nach erfolgreichem Upload schlägt fehl. Kein Erfolgshinweis ohne Datei und kein Verlust des bisherigen Logos.
|
||||
|
||||
### F26 — Anmeldemodal ohne Fokusführung
|
||||
|
||||
**Priorität:** P2, Bedienbarkeit/Barrierefreiheit.
|
||||
|
||||
**Fundstellen:** `resources/views/components/web/site-header.blade.php:102–105,158,189–236`.
|
||||
|
||||
Das eigene Modal verwendet `x-show`, `role="dialog"` und `aria-modal="true"`. Eine explizite Übernahme des Tastaturfokus, Fokusbegrenzung innerhalb des Modals und Rückgabe an den Auslöser sind im untersuchten Template nicht implementiert. Anders als ein Flux-Modal erhält dieser eigene Div-Aufbau solche Funktionen nicht allein durch ARIA-Attribute.
|
||||
|
||||
**Trigger:** Öffnen über Tastatur und anschließende Tab-Navigation. Der Fokus kann im Hintergrund bleiben bzw. außerhalb des sichtbaren Dialogs weiterlaufen. Escape ist implementiert und als positive Teilabsicherung zu berücksichtigen.
|
||||
|
||||
**Empfehlung:** Bestehende zugängliche Dialogkomponente verwenden oder Fokusübernahme, Fokusbegrenzung, Rückgabe und geeignete Hintergrund-Deaktivierung vollständig ergänzen. Kein pauschaler rechtlicher Konformitätsbefund: Ein visueller und assistiver Browserlauf fehlt.
|
||||
|
||||
**Regression:** Browserprüfung mit Tab/Shift-Tab, Escape, Öffnen/Schließen, Rückfokus und Screenreader-Beschriftung bei Desktop- und Mobilbreite.
|
||||
|
||||
## 7. Newsletter, Abrechnung und Hintergrundverarbeitung
|
||||
|
||||
### F05 — Newsletter dupliziert und überspringt Empfänger ab der zweiten Seite
|
||||
|
||||
**Priorität:** P1.
|
||||
|
||||
**Fundstellen:** `app/Services/Newsletter/CampaignSender.php:37–50`; `app/Services/Newsletter/CampaignRecipients.php:175–176`; `vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:3179–3190`; `database/migrations/2026_07_21_111646_create_newsletter_campaign_tables.php:36–46`.
|
||||
|
||||
Der Sender verwendet `chunkById(500)` auf einer Empfängerquery, die bereits nach `created_at` oder `last_login_at` sortiert. Laravel ersetzt dabei nur die Sortierung der ID-Spalte; die andere führende Sortierung bleibt bestehen.
|
||||
|
||||
**Deterministisches Beispiel:** 1.000 Benutzer mit zeitlich steigenden IDs und unterschiedlichen `created_at`-Werten, Sortierung `created_at DESC`. Die erste Seite liefert 1000 bis 501. Die zweite Seite setzt `id > 501`, sortiert aber weiterhin zeitlich absteigend und liefert 1000 bis 502 erneut. Die IDs 1 bis 500 werden nicht erreicht.
|
||||
|
||||
Die Migration und die tatsächlich abgefragten MySQL-Indizes enthalten keinen Unique-Constraint auf Kampagne/Benutzer oder Kampagne/E-Mail. Die wiederholten Empfängerzeilen können daher gespeichert und als eigene Jobs versendet werden.
|
||||
|
||||
**Auswirkung:** Doppelversand, ausgelassene Empfänger und falsche Kampagnenstatistiken. Das ist ein konkreter Grenzfehler, kein bloßer Performancevorschlag.
|
||||
|
||||
**Empfehlung:** Auswahlquery von der UI-Sortierquery trennen. Vor ID-Chunking eine eindeutige ID-Reihenfolge herstellen, beispielsweise über `reorder()`. Falls die Versandfolge fachlich relevant ist, einen passenden zusammengesetzten Cursor bzw. eine eingefrorene Versandposition einsetzen. Empfänger zusätzlich eindeutig absichern.
|
||||
|
||||
**Regression:** Mindestens 1.001 Benutzer, unterschiedliche Zeitwerte und alle angebotenen Sortierungen. Jede ausgewählte Benutzer-ID genau einmal und vollständige Abdeckung prüfen. Die vorhandenen Senderprüfungen in `tests/Feature/Admin/NewsletterCampaignTest.php:230–281` überschreiten die Chunk-Grenze nicht.
|
||||
|
||||
### F06 — MAN-Abrechnung läuft über das explizite Vertragsende hinaus
|
||||
|
||||
**Priorität:** P1.
|
||||
|
||||
**Fundstellen:** `app/Services/Billing/ManualInvoiceService.php:44–58,71–80,143–148`; `app/Models/UserPaymentOption.php:94–115`; `app/Console/Commands/GrandfatherLegacySubscriptions.php:99–100,207–209`.
|
||||
|
||||
Der Import übernimmt `valid_until_date` nach `grandfathered_until`. Die aktive Legacy-Berechtigungslogik berücksichtigt dieses Vertragsende. Weder die Rechnungsauswahl noch die erneute Prüfung unter dem Row Lock berücksichtigen es.
|
||||
|
||||
**Trigger:** Status bleibt `Grandfathered`, das explizite Vertragsende ist bereits überschritten, `current_period_end` ist fällig. Der Service erstellt weiterhin eine MAN-Rechnung und verlängert die Periode.
|
||||
|
||||
**Auswirkung:** Zugang und Abrechnung beurteilen denselben Vertrag widersprüchlich. Es können Rechnungen für einen technisch bereits als abgelaufen behandelten Vertrag entstehen. Ob solche Fälle im Bestand vorhanden sind, wurde nicht anhand personenbezogener Rechnungsdaten untersucht.
|
||||
|
||||
**Empfehlung:** Eine zentrale fachliche Abrechenbarkeitsregel verwenden und das Vertragsende sowohl bei der Auswahl als auch direkt vor dem Schreiben unter Sperre beachten. Behandlung des Endtags und einer gegebenenfalls abschließenden Abrechnung ausdrücklich festlegen.
|
||||
|
||||
**Regression:** Gestern beendeter, heute endender und unbefristeter Vertrag; auch direkter Aufruf von `invoiceFor()` muss dieselbe Grenze beachten.
|
||||
|
||||
### F14 — Newsletter-Start ist nicht gegen Parallelität und Teilabbrüche abgesichert
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Services/Newsletter/CampaignSender.php:29–75`; Empfängertabelle wie F05.
|
||||
|
||||
Der Draft-Guard prüft ein bereits geladenes Model. Empfänger werden ohne gemeinsamen Start-Lock eingefügt, anschließend wird `Sending` gesetzt und erst danach werden Jobs einzeln erzeugt.
|
||||
|
||||
**Fehlerfälle:** Zwei Requests starten denselben Entwurf; ein Start bricht mitten im Empfängerimport ab; oder der Queue-Dispatch scheitert nach einigen Jobs. Je nach Zeitpunkt bleiben doppelte Empfänger, eine teilweise befüllte Draft-Kampagne oder eine nicht mehr startbare `Sending`-Kampagne mit dauerhaft wartenden Empfängern zurück.
|
||||
|
||||
**Empfehlung:** Atomare Übernahme des Kampagnenstarts, eindeutige Empfänger und wiederaufnehmbare Einplanung aus persistentem Zustand. Beispielsweise vorbereitender Status plus Compare-and-swap/Row Lock und separater Dispatcher/Outbox. Eine große DB-Transaktion allein garantiert die Übergabe an eine externe Queue nicht.
|
||||
|
||||
**Regression:** Zwei unabhängig geladene Modelinstanzen derselben Kampagne; Ausnahme nach dem ersten Empfängerbatch; Ausnahme nach dem ersten Dispatch. Wiederanlauf muss alle Empfänger genau einmal einplanen.
|
||||
|
||||
### F15 — Versandjob kann doppelt senden und Fortschritt falsch zählen
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Jobs/SendNewsletterCampaignMail.php:32–57,80–102`.
|
||||
|
||||
Das Lesen von `Pending` ist kein atomarer Claim. Erst nach SMTP-Versand wird der Empfängerstatus geändert; Empfängerabschluss und Kampagnenzähler erfolgen in separaten Schreibvorgängen.
|
||||
|
||||
**Trigger:** SMTP nimmt eine Nachricht an, anschließend scheitert das DB-Update. Der Retry sieht weiterhin `Pending` und sendet nochmals. Bei einem Abbruch nach Empfängerupdate, aber vor Zählererhöhung bleibt der Fortschritt dagegen dauerhaft zu niedrig. Parallel laufende Instanzen können dieselbe Pending-Zeile gleichzeitig bearbeiten.
|
||||
|
||||
**Empfehlung:** Atomarer Versandclaim und idempotenter, transaktionaler DB-Abschluss. Für die Grenze SMTP/DB eine bewusste Zustellstrategie definieren: Provider-Idempotenz, falls verfügbar, oder sichtbarer Zustand „Zustellung unklar“. Exakt einmalige SMTP-Zustellung lässt sich nicht durch einen DB-Lock allein garantieren.
|
||||
|
||||
**Regression:** Fehler vor/nach SMTP-Annahme und zwischen Statusupdate und Zählerpflege; parallele Bearbeitung derselben Empfängerzeile. Wiederanlauf und Abschlussstatus prüfen.
|
||||
|
||||
### F16 — Die 50 ältesten nicht abrechenbaren Verträge können alles Weitere blockieren
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Console/Commands/GenerateManualInvoices.php:19–21,30,41–68`; `app/Services/Billing/ManualInvoiceService.php:44–58,87–107`; `routes/console.php:111–115`.
|
||||
|
||||
Der tägliche Lauf wählt standardmäßig die 50 ältesten fälligen Vereinbarungen. Nicht abrechenbare Fälle, beispielsweise ohne Rechnungsadresse oder Intervall, werden übersprungen und behalten ihre alte Fälligkeit. Ein nächster Batch wird nicht verarbeitet.
|
||||
|
||||
**Trigger:** Die ersten 50 fälligen Vereinbarungen sind unvollständig. Sie werden jeden Tag erneut ausgewählt, während die 51. vollständig abrechenbare Vereinbarung nie erreicht wird. Auch ohne ungültige Fälle ist der planmäßige Durchsatz auf 50 Vereinbarungen täglich begrenzt.
|
||||
|
||||
**Empfehlung:** Den fälligen Bestand mit begrenzten Cursor-Batches durchgehen; fehlerhafte Fälle separat markieren und erneut bearbeiten. Metriken für Rückstand und Alter des ältesten bearbeitbaren Falls ergänzen.
|
||||
|
||||
**Regression:** 50 unvollständige Vereinbarungen vor mindestens einer vollständigen. Die vollständige muss trotz der übersprungenen Fälle verarbeitet werden.
|
||||
|
||||
### F17 — KI-HTTP-Versuche können länger dauern als der Worker zulässt
|
||||
|
||||
**Priorität:** P2, bedingter Betriebsbefund.
|
||||
|
||||
**Fundstellen:** `app/Services/PressRelease/Classification/Drivers/OpenAiClassificationDriver.php:35–45`; `app/Services/PressRelease/ContentScore/Drivers/OpenAiContentScoreDriver.php:43–53`; `config/scoring.php:38,62`; `app/Jobs/ClassifyPressRelease.php:33`; `app/Jobs/ScorePressRelease.php:29`; `app/Console/Commands/RunClassificationQueue.php:23–34`; `config/queue.php:42,70`.
|
||||
|
||||
Der Content-Score-Treiber hat standardmäßig bis zu 30 Sekunden pro HTTP-Versuch; die Retry-Liste `[200, 500]` erlaubt drei Versuche. Sein mögliches Gesamtbudget liegt damit bei ungefähr 90,7 Sekunden zuzüglich sonstiger Arbeit. Die Klassifikation verwendet laut `config/scoring.php` dagegen standardmäßig 15 Sekunden und kommt auf ungefähr 45,7 Sekunden; das gleiche Überschreitungsrisiko entsteht dort erst bei entsprechend höherer Konfiguration oder zusätzlicher Laufzeit. Die Jobs besitzen kein eigenes Timeout; der Projekt-Worker setzt ebenfalls keines, sodass der Laravel-Standard von 60 Sekunden relevant ist. `retry_after` liegt standardmäßig bei 90 Sekunden.
|
||||
|
||||
**Trigger:** Mehrere langsame Transporttimeouts beim Content-Score. Der Worker kann den Prozess beenden, bevor der vorgesehene deterministische Fallback erreicht wird. Produktionsseitig anders konfigurierte Timeouts können das Verhalten ändern und wurden nicht verifiziert.
|
||||
|
||||
**Empfehlung:** HTTP-Gesamtbudget, Jobtimeout, Workerparameter und `retry_after` zusammen abstimmen. `retry_after` mit Sicherheitsabstand über dem maximalen Jobtimeout halten. Ein isoliertes Anheben nur des Worker-Timeouts kann Doppelverarbeitung begünstigen.
|
||||
|
||||
**Regression:** Kontrollierte langsame/fehlgeschlagene Treiberantworten mit realem Queue-Worker; Fallback und endgültiges Scheitern beobachten. Ein synchroner Jobtest beweist dieses Verhalten nicht.
|
||||
|
||||
### F18 — Monatskontingent-Reset ist nicht idempotent
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Console/Commands/ResetMonthlyPressReleaseQuota.php:30–34`; `routes/console.php:121–126`; `app/Services/PressRelease/PressReleaseService.php:291–296`.
|
||||
|
||||
Der Command setzt alle positiven Monatszähler auf null, ohne die bereits zurückgesetzte Periode zu speichern. Er ist am Monatsersten um 00:05 geplant.
|
||||
|
||||
**Trigger:** Publikationen zwischen Mitternacht und 00:05 werden kurz darauf wieder aus dem Verbrauch entfernt. Eine Wiederholung nach weiteren Publikationen löscht den Verbrauch nochmals. Wird der Schedulertermin verpasst, ist im Resetpfad kein automatischer Catch-up vorgesehen.
|
||||
|
||||
**Empfehlung:** Verbrauch an eine eindeutige Kalenderperiode binden und den Periodenwechsel atomar genau einmal vornehmen. Alternativ periodisierte Verbrauchsdatensätze verwenden. Nachholung beim ersten Zugriff definieren, damit ein einzelner Crontermin nicht die gesamte Kontingentlogik bestimmt.
|
||||
|
||||
**Regression:** Monatswechsel, zweimaliger Reset mit Publikation dazwischen, verspäteter Lauf. Kalenderkontingent nicht ohne fachliche Entscheidung auf Stripe-Aboperioden umstellen.
|
||||
|
||||
### F19 — Zwei Aufrufaggregatoren können dieselben Events doppelt zählen
|
||||
|
||||
**Priorität:** P2, Parallelitätsbedingung.
|
||||
|
||||
**Fundstellen:** `app/Console/Commands/AggregatePressReleaseViews.php:45–46,73–91,99–113`; Schedulerregistrierung in `routes/console.php`.
|
||||
|
||||
Counter und Fortschrittsmarke werden erfreulicherweise gemeinsam transaktional geschrieben. Die Fortschrittsmarke wird jedoch vorher gelesen und nicht unter einer gemeinsamen Sperre neu geprüft.
|
||||
|
||||
**Trigger:** Ein manueller Command-Lauf überlappt den Scheduler oder zwei unabhängig gestartete Prozesse laufen gleichzeitig. Beide lesen denselben Marker und addieren dieselben Ereignisse. `withoutOverlapping()` schützt den Scheduleraufruf, jedoch nicht automatisch jeden manuellen Einstieg.
|
||||
|
||||
**Empfehlung:** Einen für alle Einstiegspfade gemeinsamen Command-Lock verwenden oder die Watermark unter Row Lock lesen und den Batch innerhalb der geschützten Sequenz bestimmen. Laufzeitbegrenzung für dauerhaften Ereignisnachschub ergänzen.
|
||||
|
||||
**Regression:** Zwei DB-Verbindungen mit kontrolliertem Interleaving. Jeder Event darf nur einmal in `hits` eingehen; Wiederholung ohne neue Events muss wirkungslos sein.
|
||||
|
||||
### F20 — Geplante Publikationen melden trotz technischer Fehler Erfolg
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Console/Commands/PublishScheduledPressReleases.php:93–117`; `routes/console.php`, Failure-Hook für `press-releases:publish-scheduled`.
|
||||
|
||||
Technische Exceptions werden gezählt und über `report()` protokolliert. Der Command liefert anschließend trotzdem immer `SUCCESS`. Der konfigurierte `onFailure`-Alarm hängt jedoch vom fehlgeschlagenen Command ab.
|
||||
|
||||
**Trigger:** Einzelne oder alle fälligen PMs scheitern im Veröffentlichungsservice. Exit-Code-basierte Überwachung meldet einen erfolgreichen Lauf.
|
||||
|
||||
**Empfehlung:** Erwartete fachliche Ablehnungen von technischen Fehlern unterscheiden. Bei technischen Fehlern einen Fehler-Exit-Code liefern oder einen ausdrücklichen Alarm auslösen. Vorhandenes Fehlerlogging beibehalten; der Befund lautet nicht „Fehler werden überhaupt nicht protokolliert“.
|
||||
|
||||
**Regression:** Service wirft technische Exception: Fehler-Exit-Code bzw. Alarm nachweisen. Blacklistablehnung separat testen und fachlich definieren.
|
||||
|
||||
## 8. Datenbank, Importe und Performance
|
||||
|
||||
### F21 — Legacy-Import ist nicht durchgehend speicherbegrenzt oder atomar
|
||||
|
||||
**Priorität:** P2; Relevanz abhängig von weiteren Import-/Delta-Läufen.
|
||||
|
||||
**Fundstellen:** `app/Services/Import/PressReleaseImporter.php:34–58,159–170,182–265`; insbesondere die Identitätsübernahme bei `:189`.
|
||||
|
||||
Vor dem 200er-PM-Chunking werden die vollständigen Legacy-Bild- und Kontaktpivot-Tabellen geladen und gruppiert. Auch ein kleiner `--since`-Deltaimport benötigt dadurch Speicher abhängig vom Gesamtbestand.
|
||||
|
||||
Pro PM erfolgen Hauptdatensatz, Zeitstempel, Bilder, Kontakte und Importmap ohne gemeinsame Zieltransaktion. Scheitert der Import nach Speicherung der PM, aber vor dem Mapping, existiert ein Teilimport. Der Wiederanlauf identifiziert den bereits vorhandenen Datensatz nicht in allen Fällen unabhängig von der Importmap und kann seine öffentliche UUID-/Slug-Identität verändern.
|
||||
|
||||
**Empfehlung:** Relationen nur für die Legacy-IDs des aktuellen Chunks laden. Zieländerungen einschließlich Mapping je PM transaktional speichern. Vorhandene Zielidentität auch ohne Mapping anhand des stabilen Legacy-Schlüssels erkennen. Delta-Wiederanlauf ausdrücklich als fachlichen Vertrag testen.
|
||||
|
||||
**Regression:** Ausnahme nach Hauptdatensatz, aber vor Mapping; Wiederholung muss dieselbe UUID und denselben Slug behalten. Großer Mediengesamtbestand mit kleinem PM-Delta darf den Speicherbedarf nicht linear zum Gesamtbestand anwachsen lassen.
|
||||
|
||||
### F22 — E-Mail-Suche umgeht den bereits angelegten Index
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `app/Services/Auth/ContactAccessService.php:128–146`; `app/Models/User.php:186`; Indexmigration für `companies.email` und tatsächlich vorhandener `companies_email_idx`.
|
||||
|
||||
Die Firmenzuordnung fragt `LOWER(email) = ?` ab. Ein normaler Index auf `email` kann diese Funktionsbedingung in der beobachteten Query nicht verwenden. Das wurde mit zwei ausschließlich lesenden `EXPLAIN`-Abfragen überprüft; als Vergleichswert wurde eine synthetische Adresse unter `example.invalid` verwendet.
|
||||
|
||||
| Vergleich | MySQL-Zugriff | Index | Geschätzte Zeilen |
|
||||
|---|---|---|---:|
|
||||
| `LOWER(email) = ... AND deleted_at IS NULL` | `ALL` | keiner | 110.291 |
|
||||
| `email = ... AND deleted_at IS NULL` | `ref` | `companies_email_idx` | 1 |
|
||||
|
||||
Die verfügbare Datenbank verwendet für `companies.email` `utf8mb4_unicode_ci`, also eine bereits case-insensitive Kollation. Die Zahlen sind **Optimizerschätzungen**, keine gemessenen Laufzeiten und keine exakt gelesenen Datenmengen. Für `users` und `contacts` wurde in diesem Audit kein eigener EXPLAIN-Vergleich vorgenommen.
|
||||
|
||||
**Auswirkung:** Ein vorhandener Performanceindex bleibt gerade in einem häufigen Login-/Zuordnungspfad ungenutzt. Mehrere Loginverfahren und erneute Zuordnungen können diese Suche wiederholt auslösen.
|
||||
|
||||
**Empfehlung:** E-Mail-Normalisierung und Kollationsvertrag festlegen. Unter der vorhandenen MySQL-Kollation einen direkten Vergleich nutzen und Groß-/Kleinschreibungsfälle testen; bei abweichenden Datenbanken gegebenenfalls eine indizierte normalisierte Spalte verwenden. Nicht ungeprüft die gleiche Annahme auf alle Tabellen/Umgebungen übertragen.
|
||||
|
||||
**Regression:** Groß-/Kleinschreibung, führende/nachfolgende Leerzeichen und Legacy-Adressen; zusätzlich EXPLAIN für den finalen Querypfad. Erfolgskriterium ist korrekte Zuordnung mit Indexzugriff, nicht nur eine kürzere Query.
|
||||
|
||||
### 8.1 Beobachtete Größenordnung und Konsequenzen
|
||||
|
||||
Lesend aus `information_schema.TABLES` entnommene InnoDB-Schätzwerte der verfügbaren Verbindung:
|
||||
|
||||
| Tabelle | Geschätzte Zeilen |
|
||||
|---|---:|
|
||||
| `press_releases` | 134.979 |
|
||||
| `companies` | 110.291 |
|
||||
| `users` | 65.572 |
|
||||
| `contacts` | 19.018 |
|
||||
| `press_release_events` | 952.754 |
|
||||
| `activity_events` | 5.316 |
|
||||
|
||||
Diese Werte sind ungefähre Schema-/Optimierermetadaten. Es wurden dafür keine einzelnen Personen-, Kunden- oder Nachrichtendatensätze ausgelesen. Sie belegen, dass Vollscans, unbegrenzte Collection-Ladevorgänge und kleine Batch-Grenzfehler praktisch relevante Größenordnungen erreichen können.
|
||||
|
||||
### 8.2 Weitere Performanceverbesserungen
|
||||
|
||||
1. **Cacheinvalidierung gezielter machen:** `AdminPerformanceCacheObserver` verwirft bei jeder beobachteten Änderung die gesamte Liste der Admin-Cachekeys. Bei Importen oder vielen einzelnen Änderungen entstehen unnötige Löschoperationen. Zuerst Anzahl und Dauer messen; danach nach Domäne bzw. Batch invalidieren.
|
||||
2. **Abfragebudget pro Seite definieren:** Für PM-Listen, Firmenansicht und Dashboard sowohl Abfrageanzahl als auch geladene Zeilen messen. Eine konstante Queryanzahl allein schließt übergroße Relationen nicht aus.
|
||||
3. **Kein zusätzlicher Index ohne Query-Plan:** Bereits viele kombinierte Indizes sind vorhanden. Weitere Indizes erhöhen Schreib- und Migrationskosten. Kandidaten erst anhand realer WHERE-/ORDER-BY-Pfade bewerten.
|
||||
4. **Öffentliche Suche begrenzen:** Kurze Suchbegriffe fallen bewusst auf LIKE zurück. Eingabelänge, Wiederholrate, teure Sortierungen und tiefe Seiten bei großen Beständen beobachten. Keine unbelegte SQL-Injection behaupten: die betrachteten Rohbedingungen verwenden Parameterbindung.
|
||||
5. **Firmen-/Kontaktzuordnung bündeln:** Die Zuordnung führt pro Firma weitere Existenz-/Pivotabfragen aus. Nach F01/F10 kann dieser Pfad auf konsistente, gebündelte Berechtigungsdaten ausgerichtet werden; Sicherheit geht vor Queryverkürzung.
|
||||
|
||||
## 9. Tests, CI und Entwicklungsqualität
|
||||
|
||||
### F24 — Lint-Workflow prüft den eingecheckten Stil nicht verbindlich
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `.github/workflows/lint.yml:13–14,31–46`.
|
||||
|
||||
Der Workflow führt `vendor/bin/pint` im schreibenden Modus aus. Die nachfolgende Commit-Stufe ist auskommentiert; ein Diff-/Fehlergate fehlt. Der CI-Checkout kann daher erfolgreich formatiert werden, während der eigentliche Commit unverändert unformatiert bleibt. Ein erfolgreicher Workflow ist kein Nachweis, dass der eingecheckte Code bereits dem Stil entspricht.
|
||||
|
||||
**Empfehlung:** Einen bewusst prüfenden CI-Mechanismus verwenden: beispielsweise nach dem erforderlichen Formatierungslauf einen verbleibenden Git-Diff als Fehler werten. Die für dieses Projekt geltende lokale Pint-Regel und den CI-Vertrag ausdrücklich unterscheiden. Schreibrechte `contents: write` sind für einen rein prüfenden Job nicht notwendig.
|
||||
|
||||
**Regression/Abnahme:** Absichtlich unformatierter Probe-Commit muss den erforderlichen CI-Check rot machen; sauberer Commit grün. In diesem Audit wurde kein Formatter ausgeführt, da keine PHP-Datei geändert werden sollte.
|
||||
|
||||
### F25 — Hohe Testzahl, aber Lücken bei produktionsnahen und sicherheitskritischen Zuständen
|
||||
|
||||
**Priorität:** P2.
|
||||
|
||||
**Fundstellen:** `phpunit.xml:7–14,29–43`; `phpstan.neon:16–27`; `.github/workflows/tests.yml:35–61`; `tests/Feature/PressReleasePreviewTest.php:237–247`; `tests/Feature/CustomerProfileSecurityTest.php:253–270`.
|
||||
|
||||
Die Testkonfiguration erzwingt SQLite `:memory:` und synchrone Queues. Das ist für viele schnelle Featuretests sinnvoll, beweist aber keine MySQL-Zeilensperren, Fulltext-Ausführung, Kollationsdetails, Queue-Timeouts oder echte konkurrierende Transaktionen. Die CI-Datei zeigt keine ergänzende MySQL-/Queue-Matrix.
|
||||
|
||||
PHPStan arbeitet auf Level 4 mit Baseline. Die Analysepfade umfassen `app`, Bootstrap, Config, Factories, Seeder und Routen, jedoch nicht die umfangreiche PHP-Logik in Volt-Blade-Dateien. Diese Logik ist nicht schon dadurch statisch analysiert, dass das Projekt Larastan verwendet.
|
||||
|
||||
Hinzu kommen konkret irreführende bzw. zu kleine Tests: Der Soft-Delete-Test benutzt Force Delete (F04); Newslettertests überschreiten die Chunk-Grenze nicht (F05); die Recovery-Tests prüfen normale UI-Sichtbarkeit, nicht die Manipulation der öffentlichen Property (F03). Ein JS-/Browser-Testlauf ist in den betrachteten Paket- und CI-Dateien nicht eingerichtet.
|
||||
|
||||
**Empfehlung:** Bestehende schnelle Tests behalten und gezielt ergänzen: MySQL-Integration für Locks/Fulltext, echte Workerprüfung für Zeitgrenzen, Browserfälle für Fokus/Livewire-Navigation und negative Lifecycle-Tests für Rechte-/Identitätsänderungen. Geschäftskritische Logik schrittweise aus großen Volt-Dateien in bereits etablierte analysierbare Services/Actions verlagern. Baseline nur begründet verändern; sie nicht als allgemeine Fehlerablage behandeln.
|
||||
|
||||
**Abnahme:** F01–F07 erhalten jeweils einen zuvor fehlschlagenden Regressionstest; mindestens ein produktionsnaher Lauf beweist die DB-/Queue-spezifischen Fälle. Eine Code-Coverage-Prozentzahl wurde nicht ermittelt und wird nicht aus der Testanzahl abgeleitet.
|
||||
|
||||
### F27 — `composer run dev` startet standardmäßig keinen Vite-Server
|
||||
|
||||
**Priorität:** P3.
|
||||
|
||||
**Fundstellen:** `composer.json`, Script `dev`; `package.json`, Scripts `dev`, `dev:portal`, `dev:web`, `dev:all`.
|
||||
|
||||
Composer startet neben Server, Queue und Logs `npm run dev`. Dieses npm-Script gibt jedoch nur einen Hinweistext aus und beendet sich. Die tatsächlichen Vite-Server werden ausschließlich durch die spezialisierten Scripts gestartet.
|
||||
|
||||
**Auswirkung:** Der übliche Entwicklungsstart wirkt aktiv, liefert aber keine laufende Asset-Neukompilierung. Vorhandene Builddateien können den Fehler zunächst verdecken. Ohne Assets können zusätzlich Manifest-/Darstellungsfehler auftreten.
|
||||
|
||||
**Empfehlung:** Einen funktionierenden Standardstart definieren und die bewusste Auswahl einzelner Bundles zusätzlich anbieten. Den Bedarf an externem MySQL, Redis, Mailpit und Traefik im Startprozess klar dokumentieren: `docker-compose.yml` referenziert diese Infrastruktur, stellt sie selbst aber nicht vollständig bereit.
|
||||
|
||||
**Abnahme:** Aus einer vorbereiteten frischen Entwicklungsumgebung den dokumentierten Hauptbefehl starten; beide gewünschten Hotfiles/Ports und eine sichtbare Assetänderung prüfen. Kein Dependency-Upgrade dafür erforderlich.
|
||||
|
||||
### F28 — Entwicklungsdokumentation enthält widersprüchliche aktuelle Zustände
|
||||
|
||||
**Priorität:** P3.
|
||||
|
||||
**Fundstellen:** `docs/README.md:3–6,24–45`; `dev/roadmap.md:3–6`; `dev/README.md`; `resources/views/admin/README.md`; `resources/views/admin/BACKEND_STATUS.md`.
|
||||
|
||||
`docs/README.md` bezeichnet den Stand vom 12.06.2026 als Einstieg und führt inzwischen vorhandene Funktionen als noch nicht umgesetzt. `dev/roadmap.md` ist als Stand vom 20.07.2026 formuliert, während Entwicklung und Berichte unter `dev/2026-09-01` bis September weiterlaufen. Mindestens der Link auf `dev/frontend/hub-flux/PROGRESS.md` verweist auf die alte, inzwischen archivierte Struktur. Admin-Statusdateien enthalten ebenfalls ältere TODO-Stände.
|
||||
|
||||
**Auswirkung:** Neue Entwickler können vorhandene Funktionen nochmals planen, veraltete Entscheidungen als verbindlich ansehen oder wichtige offene Betriebsaufgaben übersehen. Umfangreiche Dokumentation hilft nur, wenn Gegenwart, historische Entscheidung und Archiv eindeutig unterscheidbar sind.
|
||||
|
||||
**Empfehlung:** Einen einzigen aktuellen Einstieg mit Datum, Verantwortlichkeit und Links auf Code-/Betriebsreferenzen festlegen. Historische Dokumente als solche markieren und verschobene Verweise korrigieren. Für jede neue Entscheidung den betroffenen bisherigen Entscheidungsstand ausdrücklich ersetzen.
|
||||
|
||||
**Abnahme:** Ein neuer Entwickler kann aus dem Einstieg Architektur, lokalen Start, Prüfkommandos, aktuellen Backlog und Deploymentablauf ohne widersprüchliche Statusangaben finden. Links automatisiert auf vorhandene lokale Ziele prüfen.
|
||||
|
||||
### 9.1 Entwicklungsverlauf
|
||||
|
||||
Die betrachtete lokale Git-Historie zeigt seit April 2026 folgende Commitverteilung: Mai 7, Juni 110, Juli 222, September 14. Diese Zahlen sind nur die sichtbare Historie des Checkouts, keine Produktivitätsmessung oder vollständige Teamhistorie.
|
||||
|
||||
Die jüngsten Änderungen sind fachlich fokussiert: Firmenkonsolidierungs-Trockenlauf, Konversionsmessung, API-Portalauflösung, Newsletter-Bounces, E-Mail-Index, Wiederherstellung der statischen Analyse und Entfernung mit 410. Das spricht für eine aktive Nachbesserungsphase nach dem Launch.
|
||||
|
||||
Das wiederkehrende Entwicklungsrisiko ist **teilweise Zentralisierung**: Neue Services und abgesicherte Pfade existieren, alte Alternativwege bleiben gleichzeitig aktiv. Beispiele sind der sichere `pending_email`-Dialog neben der sofortigen Fortify-Änderung, 2FA im Magic-Login neben dem direkten Google-Login und ein korrekter 410-Cacheflush neben normalem `delete()`. Künftige Änderungen sollten deshalb immer alle Einstiegspfade einer fachlichen Operation erfassen.
|
||||
|
||||
## 10. Weitere Optimierungen und gezielt offene Prüfungen
|
||||
|
||||
Die folgenden Punkte sind bewusst nicht als zusätzliche bestätigte Sicherheitslücken gezählt. Sie benötigen Messung, eine fachliche Entscheidung oder einen produktionsnahen Nachweis.
|
||||
|
||||
| Bereich | Beobachtung | Nächster sinnvoller Schritt |
|
||||
|---|---|---|
|
||||
| Authentifizierungsarchitektur | Mehrere Login- und Profiländerungspfade sind auseinanderentwickelt | Gemeinsame Identitäts-/Loginabschlusslogik und einheitlicher E-Mail-Änderungsservice |
|
||||
| Google-Identität | `SocialAuthService` verknüpft primär per E-Mail und überschreibt Providerdaten | Umgang mit stabiler Provider-ID, verifizierter Adresse, Providerwechsel und Account-Linking ausdrücklich definieren; nicht als nachgewiesene Google-Tokenfälschung einstufen |
|
||||
| Rollen/Presets | Preset-Routen sind nicht mit einer eigenen Managementpermission eingeschränkt | Entscheiden, ob Editoren globale Voreinstellungen ändern dürfen; dann Route und Actions symmetrisch absichern |
|
||||
| MAN-Perioden | `addMonth()` / `addYear()` können an Monats-/Schaltjahresgrenzen überlaufen | Vertraglichen Periodenanker definieren; 31. Januar und 29. Februar testen |
|
||||
| MAN-Steuerdaten | MAN-Resolver und Checkout nutzen unterschiedliche Prüftiefe bei USt-IDs | Technischen Datenvalidierungsvertrag angleichen und fachlich freigeben; keine Steuerrechtsbewertung aus diesem Audit ableiten |
|
||||
| Legacy-Aufrufzahlen | Reimport schreibt absolute Legacy-`hits`, neuer Aggregator addiert eigene Events | Bei weiteren Delta-Läufen Eigentümerschaft des Zählers definieren; Verlust bereits aggregierter neuer Aufrufe verhindern |
|
||||
| Mediencache | Öffentliche Medien erhalten `public, max-age=86400` | Anforderungen an Entfernungsfristen, CDN-Purge und versionierte URLs prüfen; gespeicherte Browserkopien sind nicht durch DB-Änderung widerrufbar |
|
||||
| Bildverarbeitung | MIME-/Bytegrenzen vorhanden, GD-Verarbeitung im Uploadpfad | Maximale Pixelmaße und Dekompressions-/Speicherbudget prüfen; Tests mit großen stark komprimierten Bildern |
|
||||
| Newsletter-Anmeldung | API erzeugt unbestätigte Einträge; Sync-Provider ist teilweise Platzhalter | End-to-End bestätigen, wer Bestätigungsmail, DOI-Abschluss und externen Sync tatsächlich übernimmt; keine funktionierende Integration aus einem 201 allein ableiten |
|
||||
| Queue-Priorisierung | Klassifikation und Default-Jobs teilen laut Runbook Worker | Bei Last separate Kapazität für Transaktionsmails/Newsletter/KI erwägen; Wartezeiten zuerst messen |
|
||||
| Retention | Events/Magic Links besitzen eigene Bereinigung; weitere Protokolle wachsen | Aufbewahrungs- und Archivregeln pro Datentyp festlegen, einschließlich API-Logs, KI-Audits und Versandprotokollen |
|
||||
| Abhängigkeiten | `npm i` statt `npm ci` in Test-CI; kein Dependency-Auditgate in den betrachteten Workflows | Reproduzierbare Installation und gesonderte Security-Prüfung einrichten; Updates nur nach Kompatibilitätsprüfung |
|
||||
| Sicherheitsheader | Grundheader vorhanden; keine CSP in `SecurityHeaders` | Eine zu Livewire/Alpine und bewusst erlaubten Footer-Scripts passende CSP zunächst beobachten; fehlende CSP allein ist kein XSS-Nachweis |
|
||||
| Proxy/Hosts | `trustProxies('*')` und Host-Whitelist vorhanden, einschließlich IP-/localhost-Ausnahmen | Reale Proxykette und Erreichbarkeit des Appports prüfen; Schutz vor manipulierten Forwarded-Headern nicht allein aus lokaler Config ableiten |
|
||||
| Formulare/Assets | Zwei Vite-Bundles und teilweise historische Kommentare zu Alpine | Browser-Smokes für Bundlewechsel, Livewire-Navigation, Fehlerscroll und erneute Initialisierung |
|
||||
| Große Volt-Komponenten | UI und Geschäftslogik eng gekoppelt | Schrittweise entlang tatsächlicher Aktionen zerlegen: Teilen, Boost, Nachweis, Korrektur, DSGVO-Anfrage, Abrechnung |
|
||||
| PDF-Erzeugung | Umfangreicher eigener Renderer | Repräsentative Unicode-, Umbruch-, Mehrseiten- und Randfall-PDFs visuell vergleichen; Parser-/Layouttests ergänzen |
|
||||
| Betriebliche Nachweise | Runbook vorhanden, tatsächliche Cron-/Worker-/Backupzustände nicht geprüft | Wiederherstellungstest und Alarmtest mit dokumentiertem Ergebnis; RTO/RPO und Zuständigkeit festlegen |
|
||||
|
||||
## 11. Priorisierter Umsetzungsplan
|
||||
|
||||
Alle folgenden Schritte sind **Empfehlungen für Folgearbeit**. In diesem Audit wurde keiner dieser Eingriffe umgesetzt.
|
||||
|
||||
### Paket A — Identitäts- und Veröffentlichungsgrenzen schließen
|
||||
|
||||
**Reihenfolge:** F01 → F02/F03 → F04 → F07.
|
||||
|
||||
1. Unverifizierte E-Mail als Grundlage für Berechtigungen ausschließen; alten Fortify-Profilpfad konsolidieren.
|
||||
2. Google in die gleiche 2FA-Abschlusslogik führen; Sicherheitsaktionen mit erneutem Identitätsnachweis schützen.
|
||||
3. Portalübergreifende Abfragen von Soft-Delete-Ausnahmen trennen; Medien-/Vorschauzugriff mitprüfen.
|
||||
4. KI-Ergebnisse und Freigaben an eine Inhaltsversion binden.
|
||||
|
||||
**Abnahmekriterium:** Die jeweiligen negativen Regressionstests schlagen vor dem Fix fehl und danach fehlerspezifisch nicht mehr an. Bestehende reguläre Passwort-, Magic-, Google- und Veröffentlichungsflüsse funktionieren weiter. Für F01 zusätzlich eine gezielte Bestandsprüfung automatisch verliehener Rechte planen.
|
||||
|
||||
### Paket B — Versand und Rechnungslogik stabilisieren
|
||||
|
||||
**Reihenfolge:** F05/F06 → F16 → F14/F15.
|
||||
|
||||
1. Newsletter-Chunking und Eindeutigkeit korrigieren, bevor größere Kampagnen erneut gestartet werden.
|
||||
2. Vertragsende im MAN-Auswahl- und Schreibpfad beachten.
|
||||
3. Rechnungslauf gegen blockierende unvollständige Altfälle absichern.
|
||||
4. Versandstart und Zustellung wiederaufnehmbar machen; Zustellunsicherheit offen behandeln.
|
||||
|
||||
**Abnahmekriterium:** Über 1.000 Empfänger werden vollständig und eindeutig eingeplant; beendete Verträge erzeugen keine unzulässige Folgeperiode; Fehler in einem Datensatz blockieren keine anderen. Keine Tests mit echten Mailadressen oder echten Zahlungen ausführen.
|
||||
|
||||
### Paket C — Rechtewechsel, Cache und Eingabegrenzen
|
||||
|
||||
**Umfang:** F08–F13 sowie F23.
|
||||
|
||||
Rechte erneut pro Action prüfen, Superadmin-Deaktivierung wirksam machen, automatischen Rechteentzug definieren, API-Parameter validieren, Melde-URL korrekt auflösen, öffentliche Caches konsistent invalidieren und Uploadfehler sauber behandeln.
|
||||
|
||||
**Abnahmekriterium:** Tests ändern Rechte, Datenzustand und Storageverfügbarkeit zwischen zwei Requests. Ein ausschließlich erfolgreicher Erstdurchlauf genügt nicht.
|
||||
|
||||
### Paket D — Scheduler, Queue und Importwiederanlauf
|
||||
|
||||
**Umfang:** F17–F21.
|
||||
|
||||
Zeitbudgets harmonisieren, Monatsverbrauch periodisieren, Aggregation gegen gleichzeitige Einstiege schützen, technische Publikationsfehler alarmierbar machen und Importtransaktionen/Batchspeicher korrigieren.
|
||||
|
||||
**Abnahmekriterium:** Wiederholung und Unterbrechung sind ausdrücklich getestete Normalfälle. Ein versäumter oder doppelt gestarteter Hintergrundlauf darf keine stillen fachlichen Mehrfachwirkungen verursachen.
|
||||
|
||||
### Paket E — Schnelle Optimierungen und Qualitätsgates
|
||||
|
||||
**Umfang:** F22, F24–F28 und ausgewählte Punkte aus Abschnitt 10.
|
||||
|
||||
| Maßnahme | Erwarteter Nutzen | Relativer Aufwand / Risiko |
|
||||
|---|---|---|
|
||||
| Direkter indizierter Firmen-E-Mail-Vergleich nach Kollationsprüfung | Konkrete Vermeidung eines Vollscans | Klein; Normalisierungsregeln testen |
|
||||
| Unter-/Obergrenze für API-Pagination | Begrenzter Speicher- und Queryaufwand | Klein; API-Fehlervertrag beachten |
|
||||
| Technische Schedulerfehler mit Fehlerstatus melden | Vorhandene Alarme werden aussagekräftig | Klein; fachliche Ablehnungen trennen |
|
||||
| Gutschriften erneut autorisieren | Rechteentzug sofort wirksam | Klein; Lifecycle-Test erforderlich |
|
||||
| Lintjob mit verbindlichem Diff-Gate | Formatierung des tatsächlichen Commits gesichert | Klein |
|
||||
| Funktionierender Standard-Devstart | Weniger Asset-/HMR-Verwirrung | Klein; beide Bundles prüfen |
|
||||
| Dokumentationseinstieg aktualisieren | Schnellere Orientierung und weniger Doppelarbeit | Klein |
|
||||
| MySQL-/Worker-/Browserchecks ergänzen | Produktionsrelevante Regressionen auffindbar | Mittel; gezielt beginnen |
|
||||
| Große Volt-Dateien fachlich zerlegen | Bessere Prüfbarkeit und statische Analyse | Mittel bis groß; kein pauschaler Umbau |
|
||||
|
||||
Die Aufwandsklassen sind relative Planungswerte, keine belastbaren Zeitschätzungen. Sicherheits- und Datenbestandsfolgen können den Aufwand wesentlich erhöhen.
|
||||
|
||||
## 12. Konkreter Test- und Abnahmeplan
|
||||
|
||||
### 12.1 Höchste Priorität
|
||||
|
||||
| Testpaket | Prüffälle | Passende vorhandene Anknüpfung |
|
||||
|---|---|---|
|
||||
| Identität/Firmenrechte | Unverifizierte E-Mail, Rückwechsel, legitime Verifikation, Kontaktentzug | `Auth/ContactAccessTest.php`, `CustomerProfileSecurityTest.php`, `Auth/AccessGatesTest.php` |
|
||||
| Login/2FA | Google mit TOTP, Magic mit TOTP, Passwort mit TOTP, inaktive Konten | `Auth/GoogleLoginTest.php`, `Auth/TwoFactorLoginTest.php`, `Auth/MagicLinkLoginTest.php` |
|
||||
| Sensitive Actions | Recovery-Propertymutation, Disable/Regenerate ohne erneuten Nachweis, Rechteentzug | `CustomerProfileSecurityTest.php`, `Admin/AdminGrantTest.php` |
|
||||
| Löschen/Sichtbarkeit | Soft Delete versus Force Delete; PM/Firma/Kontakt; Vorschau/Medien/API | `PressReleasePreviewTest.php`, `SecurityHardeningTest.php`, `Api/V1/PressReleaseApiTest.php` |
|
||||
| Newsletter | Mehrere Chunks, Eindeutigkeit, Parallelstart, Teilabbruch, SMTP-/DB-Fehler | `Admin/NewsletterCampaignTest.php` |
|
||||
| MAN-Rechnung | Vertragsende, Blockade durch 50 ungültige Fälle, Monatsende/Schaltjahr | `Billing/ManualInvoiceGenerationTest.php` |
|
||||
| KI-Inhaltsversion | Textänderung während Prüfung, alte Antwort nach neuer Antwort | `PressReleaseClassificationJobTest.php`, `PressReleaseContentScoreTest.php` |
|
||||
|
||||
### 12.2 Produktionsnahe Ergänzungen
|
||||
|
||||
- **MySQL:** Parallelität mit zwei Verbindungen, echte Zeilensperren, Unique-Konflikte, Slug-/E-Mail-Kollation, Fulltext und EXPLAIN. SQLite-Tests parallel behalten.
|
||||
- **Queue:** Reale Worker mit den vorgesehenen Timeouts und `retry_after`, künstliche Treiberverzögerung, Prozessabbruch und Wiederanlauf; keine echten externen Kosten verursachen.
|
||||
- **Browser:** Login-/Kontowechsel, Domain-/Bundlewechsel, Mobilnavigation, Fokus im Dialog, Validierungsanzeige und Tastaturbedienung.
|
||||
- **Betrieb:** Testalarm bei technischem Schedulerfehler; Nachweis, dass ein älterer Pending-/Sending- oder Abrechnungsrückstand sichtbar wird.
|
||||
- **Dateien:** Fehlgeschlagene Writes, DB-Abbruch nach Write, sehr große Pixelmaße, abgelaufene/gelöschte Medienlinks.
|
||||
|
||||
### 12.3 Reihenfolge der Prüfungen nach Korrekturen
|
||||
|
||||
Zuerst die jeweils betroffenen konkreten Pest-Dateien über Sail ausführen. Danach die betroffenen Fachgruppen, PHPStan und schließlich vor Freigabe die gesamte Suite sowie die ergänzten MySQL-/Worker-/Browserprüfungen. Formatierung nur im Rahmen einer autorisierten Codeänderung durchführen.
|
||||
|
||||
Beispielhafte vorhandene Dateiaufrufe:
|
||||
|
||||
```bash
|
||||
vendor/bin/sail artisan test --compact tests/Feature/Auth/GoogleLoginTest.php
|
||||
vendor/bin/sail artisan test --compact tests/Feature/Auth/ContactAccessTest.php
|
||||
vendor/bin/sail artisan test --compact tests/Feature/CustomerProfileSecurityTest.php
|
||||
vendor/bin/sail artisan test --compact tests/Feature/PressReleasePreviewTest.php
|
||||
vendor/bin/sail artisan test --compact tests/Feature/Admin/NewsletterCampaignTest.php
|
||||
vendor/bin/sail artisan test --compact tests/Feature/Billing/ManualInvoiceGenerationTest.php
|
||||
vendor/bin/sail bin phpstan analyse --memory-limit=1G --no-progress
|
||||
vendor/bin/sail composer audit --locked --format=json
|
||||
vendor/bin/sail npm audit --json
|
||||
```
|
||||
|
||||
Diese Liste ist ein Folgeplan und keine Behauptung, dass die Befehle hier erfolgreich liefen.
|
||||
|
||||
## 13. Durchgeführte Prüfungen und Ausführungsprotokoll
|
||||
|
||||
| Prüfung | Tatsächliches Ergebnis |
|
||||
|---|---|
|
||||
| Git-Ausgangszustand | `git status --short` zu Beginn leer; geprüfter HEAD oben festgehalten |
|
||||
| Dateiinventar und Größen | Erfasst; siehe Abschnitt 3.2 |
|
||||
| Boost `application-info` | Erfolgreich; tatsächliche Paketversionen erfasst |
|
||||
| Boost `database-schema`, Summary | Erfolgreich; Tabellen und Spalten lesend betrachtet |
|
||||
| Index-/Kollationsmetadaten | Erfolgreich; u. a. Empfängertabelle, PM-Slugindex und Firmen-E-Mail |
|
||||
| Zwei Firmen-E-Mail-EXPLAINs | Erfolgreich; Fullscan versus Indexzugriff, siehe F22 |
|
||||
| InnoDB-Tabellenschätzungen | Erfolgreich; keine exakten Nutzdatencounts, siehe 8.1 |
|
||||
| Framework-/Livewire-/Fortify-Dokumentation | Über Boost abgefragt; sicherheitsrelevante Aussagen zusätzlich am installierten Framework-Code geprüft |
|
||||
| Bestehende relevante Tests | Lesend geprüft, nicht als bestanden gewertet |
|
||||
| Audit-Dokumentprüfung | 28 eindeutige Detailbefunde und passende Prioritätszählung; Dateireferenzen und Zeilengrenzen geprüft. Der in F28 bewusst beanstandete historische Pfad ist erwartungsgemäß nicht vorhanden. |
|
||||
| Abschließender Git-Zustand | Ausschließlich `dev/2026-09-11-audit/` neu; keine Änderung an versionierten Anwendungsdateien |
|
||||
| `vendor/bin/sail artisan route:list --except-vendor --json --no-interaction` | Exit 1: Docker/Podman nicht verfügbar |
|
||||
| `vendor/bin/sail up -d` | Exit 1: Docker/Podman nicht verfügbar |
|
||||
| `vendor/bin/sail artisan test --compact --filter='GoogleLogin\|TwoFactorLogin\|ContactAccess\|PressReleasePreview\|NewsletterCampaign\|ManualInvoice'` | Exit 1 vor Teststart: Docker/Podman nicht verfügbar |
|
||||
| `vendor/bin/sail bin phpstan analyse --memory-limit=1G --no-progress` | Exit 1 vor Analyse: Docker/Podman nicht verfügbar |
|
||||
| `vendor/bin/sail composer audit --locked --format=json` | Exit 1 vor Advisory-Prüfung: Docker/Podman nicht verfügbar |
|
||||
| `vendor/bin/sail npm audit --json` | Exit 1 vor Advisory-Prüfung: Docker/Podman nicht verfügbar |
|
||||
| Frontend-Build / Browserlauf | Nicht durchgeführt; kein erfolgreicher Laufzeit-/Darstellungsnachweis |
|
||||
| Formatter / Migration / Versand / Abrechnung | Nicht ausgeführt; kein Anwendungscode verändert |
|
||||
|
||||
**Interpretation:** Die Umgebung verhindert dynamische Validierung, nicht die oben beschriebenen statischen Befunde. Umgekehrt beweisen ein großer Testbestand, vorhandene CI-Dateien und installierte Pakete keine aktuelle fehlerfreie Ausführung. Weder „alle Tests grün“ noch „keine Sicherheitslücken in Abhängigkeiten“ wäre auf Basis dieser Sitzung eine zulässige Aussage.
|
||||
|
||||
## 14. Quellen und Nachvollziehbarkeit
|
||||
|
||||
### 14.1 Primäre Projektquellen
|
||||
|
||||
- Routen: `bootstrap/app.php`, `routes/domains.php`, `routes/auth.php`, `routes/admin.php`, `routes/customer.php`, `routes/api.php`, `routes/legacy-api.php`, `routes/web.php`, `routes/legal.php`, `routes/console.php`.
|
||||
- Zugriffsgrenzen: `app/Authorization/Permission.php`, `app/Policies/*`, Auth-/Panel-/API-Middleware, Auth-Services und Login-/Verifizierungslistener.
|
||||
- Geschäftslogik: `app/Services/PressRelease/*`, `app/Services/Billing/*`, `app/Services/Newsletter/*`, `app/Services/Import/*`, Jobs und zugehörige Commands.
|
||||
- Oberfläche: kritische Volt-Komponenten unter `resources/views/livewire`, öffentliche Layouts/Komponenten, Vite-Konfiguration, JavaScript und CSS-Einstiegspunkte.
|
||||
- Datenmodell: betreffende Eloquent-Modelle, Migrationsdateien und lesend abgefragte MySQL-Metadaten.
|
||||
- Entwicklungsqualität: `composer.json`, `package.json`, Lockdateien als Bestand, `phpunit.xml`, `phpstan.neon`, `phpstan-baseline.neon`, `tests/`, `.github/workflows/`, Git-Historie.
|
||||
- Projektentwicklung: `dev/README.md`, `dev/roadmap.md`, `dev/2026-09-01/`, `dev/betrieb/deploy-runbook.md`, `docs/README.md` und einschlägige Statusreferenzen.
|
||||
|
||||
Datei-/Zeilenverweise gelten für den oben genannten Commit. Historische Kommentare und frühere Auditnummern im Code wurden nicht ungeprüft als aktuelle Tatsachen übernommen.
|
||||
|
||||
### 14.2 Dokumentation zur Gegenprüfung
|
||||
|
||||
- [Livewire 4: Security](https://livewire.laravel.com/docs/4.x/security) — öffentliche Actions/Properties und serverseitige Autorisierung.
|
||||
- [Livewire 4: Locked](https://livewire.laravel.com/docs/4.x/attribute-locked) — Schutz von Properties; ersetzt keine fachliche Berechtigungsprüfung.
|
||||
- [Laravel 12: Authorization](https://laravel.com/docs/12.x/authorization) — Gates, Policies und `before`-Prüfungen.
|
||||
- [Laravel 12: Fortify](https://laravel.com/docs/12.x/fortify) — Loginpipeline, 2FA und Passwortbestätigung.
|
||||
- [Laravel 12: Queues](https://laravel.com/docs/12.x/queues) — Worker-Timeouts und `retry_after`.
|
||||
- [Laravel 12: Database Testing](https://laravel.com/docs/12.x/database-testing) — Testisolation und Datenbanktests.
|
||||
|
||||
Die Boost-Suche lieferte bei einer Fortify-Abfrage einen als `latest` bezeichneten Auszug aus dem 13.x-Dokumentationszweig. Dieser wurde nicht als Beleg einer Laravel-12-spezifischen API übernommen; entscheidend waren für F01/F03 die tatsächlich installierten Fortify-Routen und Actions. Entsprechend wurden auch Pagination und Chunking direkt am installierten Laravel-Code gegengeprüft.
|
||||
|
||||
## 15. Freigabebild nach diesem Audit
|
||||
|
||||
**Anwendungscode unverändert. Optimierungen wurden ausschließlich beschrieben.**
|
||||
|
||||
Für eine belastbare technische Freigabe fehlen die Korrektur bzw. explizite Bearbeitung der P1-Befunde und die anschließend erfolgreichen Regressionen in einer ausführbaren Umgebung. Die größte Verbesserung ist nicht zusätzliche Funktionsmenge, sondern ein einheitlicher Vertrag pro fachlicher Operation: eine Identitätsgrundlage, eine Berechtigungsentscheidung, eine Inhaltsversion, eine Löschsemantik und ein wiederholbarer Hintergrundprozess.
|
||||
|
||||
Die vorhandenen zentralen Services, Datenbankstrukturen und Tests bieten dafür eine gute Ausgangsbasis. Der empfohlene Weg ist eine Reihe kleiner, fachlich abgegrenzter Änderungen mit jeweils konkretem Fehlernachweis und Regressionstest.
|
||||
217
dev/2026-09-11-audit/ENTWICKLUNGSPLAN.md
Normal file
217
dev/2026-09-11-audit/ENTWICKLUNGSPLAN.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# Entwicklungsplan zum Audit vom 11.09.2026
|
||||
|
||||
**Stand:** 18.09.2026 (Entscheidungen getroffen, Umsetzung läuft)
|
||||
**Grundlage:** [AUDIT.md](AUDIT.md) (geprüfter Commit `5d0570b`), abgeglichen mit HEAD `24c1bbf` auf `technik/betriebsfixes-2026-09`
|
||||
**Zweck:** Nur die Befunde, die am aktuellen Code belegt sind, in eine abarbeitbare Reihenfolge bringen. Jeder Punkt hat einen konkreten Fix, einen Regressionstest und eine Aufwandsklasse.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ergebnis des Abgleichs
|
||||
|
||||
Der Audit hält. Von 28 Befunden sind 25 am aktuellen Stand bestätigt, zwei nur teilweise (F08, F17), einer ist zwar richtig, aber praktisch ohne Relevanz (F21). Kein Befund wurde durch die Commits seit dem 11.09. behoben, obwohl mehrere der betroffenen Dateien angefasst wurden (`ContactAccessService`, `ManualInvoiceService`, `PressReleaseService`, `NewsroomDetailPageData`, `press-kits/show`).
|
||||
|
||||
Was über den Audit hinausgeht und bei der Prüfung aufgefallen ist:
|
||||
|
||||
- **F05 ist schlimmer als beschrieben.** Auf einer Kopie der Live-Daten liefert der Newsletter-Versand an alle 59.705 berechtigten Nutzer genau 500 eindeutige Empfänger, davon 480 doppelt. Die übrigen 59.205 werden nie erreicht. Der Grund ist `chunkById` hinter einer `created_at`-Sortierung; Laravel entfernt nur eine vorhandene `id`-Sortierung, nicht die führende.
|
||||
- **F15 hat einen zweiten Fehler durch die Mail-Sperrliste vom 15.09.** Ist ein Empfänger gesperrt, bricht `GateOutgoingMail` den Versand ab, der Job wertet den Rückgabewert nicht aus und markiert den Empfänger als „gesendet“. Nach einer Pause durch den Circuit Breaker landen Empfänger endgültig auf „Failed“, und `queue:retry` holt sie nicht nach.
|
||||
- **F04 hat eine Fundstelle, die der Audit nicht kennt.** Die öffentliche Meldungsseite (`ReleaseDetailPageData.php:76-80`) fällt auf `Contact::withoutGlobalScopes()` zurück und zeigt damit gelöschte Kontakte mit Name, Mail und Telefon öffentlich an.
|
||||
- **F04 im Kundenpanel ist ein sichtbarer Bug.** Nach „Kontakt löschen“ im Pressefach bleibt der Kontakt in der Liste und ist weiter editierbar (`press-kits/show.blade.php:273-306`).
|
||||
- **F22 ist gemessen.** `LOWER(email)` auf `companies` (114.100 Zeilen) ergibt einen Vollscan mit 108.424 geschätzten Zeilen; der direkte Vergleich nutzt `companies_email_idx` mit 1 Zeile. Die Abfrage läuft bei jedem Login. Die Kollation ist bereits `utf8mb4_unicode_ci`, `LOWER()` bringt semantisch nichts.
|
||||
|
||||
### Befundtabelle
|
||||
|
||||
| ID | Audit | Prüfung an HEAD | Eigene Einordnung | Block |
|
||||
|---|---|---|---|---|
|
||||
| F01 | P1 | bestätigt, Route aktiv, Kette vollständig | **kritisch**, sofort | 1 |
|
||||
| F02 | P1 | bestätigt | mittel | 1 |
|
||||
| F03 | P1 | bestätigt, `showRecoveryCodes` ohne `#[Locked]` | mittel, Fix klein | 1 |
|
||||
| F04 | P1 | bestätigt, plus zwei Fundstellen mehr, eine falsch benannt | mittel (PII öffentlich, Panel-Bug) | 3 |
|
||||
| F05 | P1 | bestätigt und auf Live-Kopie bewiesen | **kritisch**, vor der nächsten Kampagne | 2 |
|
||||
| F06 | P1 | bestätigt, aber latent: kein Datensatz hat `grandfathered_until` | niedrig heute, Fix klein | 5 |
|
||||
| F07 | P1 | bestätigt, `content_hash` wird nur für Duplikate genutzt | mittel, Fenster: Admin-/Korrektur-Bearbeitung im Status Review | 4 |
|
||||
| F08 | P2 | teilweise: ohne Snapshot kein Aufruf möglich, nur Stale-Session-Fall | niedrig, Fix klein | 1 |
|
||||
| F09 | P2 | bestätigt per Tinker | niedrig bis mittel, Einzeiler | 1 |
|
||||
| F10 | P2 | bestätigt, keinerlei Entzugslogik, Pivot ohne Herkunft | mittel, macht F01 dauerhaft | 1 |
|
||||
| F11 | P2 | bestätigt, nur negative `per_page` unbegrenzt, `status[]` gibt 500 | niedrig bis mittel | 3 |
|
||||
| F12 | P2 | bestätigt, Canonical-Arbeit vom 18.09. hat die Report-Controller nicht erfasst | mittel-niedrig | 3 |
|
||||
| F13 | P2 | bestätigt, bis 1 h Stale | mittel | 3 |
|
||||
| F14 | P2 | bestätigt | mittel, zusammen mit F05 | 2 |
|
||||
| F15 | P2 | bestätigt, plus Sperrlisten-Fehler | mittel | 2 |
|
||||
| F16 | P2 | bestätigt, aktuell 4 fällige Fälle, alle nicht abrechenbar | niedrig | 5 |
|
||||
| F17 | P2 | teilweise: nur der Score-Job kann 60 s reißen | niedrig bis mittel, Einzeiler | 4 |
|
||||
| F18 | P2 | bestätigt | mittel (verpasster Cron = Monat ohne Kontingent) | 4 |
|
||||
| F19 | P2 | bestätigt, kein `Isolatable` | niedrig, Einzeiler | 4 |
|
||||
| F20 | P2 | bestätigt, Runbook verspricht Alarm, der nie kommt | niedrig bis mittel, Einzeiler | 4 |
|
||||
| F21 | P2 | bestätigt, Importer ist stillgelegt | **nicht aufgenommen** | – |
|
||||
| F22 | P2 | bestätigt und gemessen | mittel, Einzeiler je Stelle | 3 |
|
||||
| F23 | P2 | bestätigt | niedrig bis mittel | 3 |
|
||||
| F24 | P2 | bestätigt | klein | 6 |
|
||||
| F25 | P2 | bestätigt; die irreführenden Tests werden in den Blöcken 2 und 3 ersetzt | die breite MySQL-/Worker-Matrix ist **aufgeschoben** | 6 |
|
||||
| F26 | P2 | bestätigt, kein Fokus-Plugin geladen | niedrig | 6 |
|
||||
| F27 | P3 | bestätigt | klein | 6 |
|
||||
| F28 | P3 | bestätigt, Link auf `dev/frontend/hub-flux/PROGRESS.md` ist tot | klein | 6 |
|
||||
|
||||
Abschnitt 10 des Audits (unbelegte Beobachtungen) wurde nicht geprüft und ist nicht im Plan. Die drei übrigen Dateien im Ordner sind keine Audit-Befunde: Bildkennzeichnung ist eine eigene Spezifikation mit Feedback-Audit (noch nicht umgesetzt), Kontakterreichbarkeit ist seit den Commits `ada41cb` ff. umgesetzt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Aufwandsskala und Arbeitsregeln
|
||||
|
||||
| Kürzel | Bedeutung |
|
||||
|---|---|
|
||||
| S | eine Stunde bis ein halber Tag |
|
||||
| M | ein bis drei Tage |
|
||||
| L | länger oder abhängig von einer Entscheidung |
|
||||
|
||||
Für jeden Punkt gilt: zuerst der Regressionstest, der am heutigen Code rot ist, dann der Fix, dann `php artisan test --compact` für die betroffene Datei, am Blockende die gesamte Suite und PHPStan. Kein Block wird ohne Deploy abgeschlossen. Blöcke 1 und 2 sind voneinander unabhängig und können parallel laufen; Block 1 hat Vorrang, weil er ausnutzbar ist.
|
||||
|
||||
---
|
||||
|
||||
## Block 1 — Identität und Rechte (F01, F10, F03, F02, F09, F08)
|
||||
|
||||
Der Kern: Firmenrechte entstehen aus einer E-Mail-Übereinstimmung, die nie auf Verifikation geprüft wird, und werden nie wieder entzogen. Alles andere in diesem Block sind Nebenwege, die dieselbe Regel unterlaufen.
|
||||
|
||||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||||
|---|---|---|---|
|
||||
| 1.1 | Fortify-Feature `updateProfileInformation` in `config/fortify.php:150` abschalten. Die Kunden-UI nutzt den Endpunkt nicht; sie hat den `pending_email`-Prozess. Test: `PUT /user/profile-information` liefert 404. | S | E1 |
|
||||
| 1.2 | `ContactAccessService::syncMembershipsForEmail()` verlangt `hasVerifiedEmail()` und `is_active`. Gilt für alle Aufrufer, nicht nur den Login-Listener. Test: unverifizierter Nutzer mit passender Firmenadresse erhält keinen Pivot; nach Verifikation schon. | S | keine |
|
||||
| 1.3 | Herkunft am Pivot speichern: Spalte `company_user.granted_via` (`auto`, `manual`, `owner`). Migration klein, Tabelle klein. Bestehende Zeilen: `manual`, wenn sie nicht durch den Sync erklärbar sind (siehe 1.4). | S | E2 |
|
||||
| 1.4 | Bestandsprüfung als Befehl mit `--dry-run`: alle `responsible`-Pivots, deren Nutzer-Mail heute weder zur Firma noch zu einem Kontakt der Firma passt. Ausgabe als Liste, keine Löschung. Danach Entscheidung je Zeile. | S | 1.3 |
|
||||
| 1.5 | Entzug beim Login-Sync: `auto`-Pivots mit `responsible`, die keinen aktuellen E-Mail-Treffer mehr haben, auf `member` senken. `manual` und `owner` bleiben unberührt. Test: Kontakt löschen oder Adresse ändern, erneut anmelden, Schreibrecht ist weg. | M | 1.3, E2 |
|
||||
| 1.6 | `security.blade.php`: `#[Locked]` auf `showRecoveryCodes`; die Codes nur im Render nach einer Action ausgeben, nicht aus einer Property. Test: `set('showRecoveryCodes', true)` scheitert. | S | keine |
|
||||
| 1.7 | Erneuter Nachweis vor `disableTwoFactorAuthentication()`, `regenerateRecoveryCodes()` und `updateEmail()`: aktueller TOTP-Code oder Recovery-Code. Das funktioniert auch für Konten ohne Passwort. Test: ohne Code 422, mit Code Erfolg. | M | E3 |
|
||||
| 1.8 | `GoogleController.php:50`: 2FA-Zweig aus `MagicLinkConsumeController.php:73-81` übernehmen; danach die drei Login-Abschlüsse in einen Service ziehen. Test in `GoogleLoginTest`: Konto mit TOTP endet als Gast auf der Challenge. | S | keine |
|
||||
| 1.9 | `AppServiceProvider.php:125`: `Gate::before` nur für `is_super_admin && is_active`. Test: deaktivierter Superadmin mit Session bekommt 403 auf Admin- und Kundenroute. | S | keine |
|
||||
| 1.10 | `routes/admin.php:101-104`: Nutzer-Detailroute unter `can:users:manage` stellen, zusätzlich `authorize()` in beiden Grant-Actions. Test: Rechteentzug bei offener Komponente, danach `grantCredits` scheitert. | S | keine |
|
||||
|
||||
**Entscheidungen**
|
||||
|
||||
- **E1** Fortify-Profiländerung abschalten (empfohlen) oder auf den `pending_email`-Prozess umbiegen. Abschalten ist ein Einzeiler und schließt den Einstieg vollständig; niemand nutzt den Endpunkt.
|
||||
- **E2** Entzugsregel: auto-vergebene `responsible` ohne aktuellen Treffer werden beim nächsten Login auf `member` gesenkt (empfohlen), oder komplett entfernt. Absenken ist reversibel und verliert keine manuellen Rechte.
|
||||
- **E3** Nachweisform für Sicherheitsaktionen: TOTP-/Recovery-Code (empfohlen, funktioniert für alle Kontotypen) oder Passwort mit Sonderweg für passwortlose Konten.
|
||||
|
||||
**Abnahme:** Die Kette aus F01 (fremde Adresse setzen, neu anmelden, zurückwechseln) ist als Test in `ContactAccessTest` abgebildet und hinterlässt keinen Pivot. Alle Login-Wege (Passwort, Magic Link, Google) landen bei aktivem TOTP auf der Challenge. Ergebnis von 1.4 ist gesichtet.
|
||||
|
||||
---
|
||||
|
||||
## Block 2 — Newsletter (F05, F14, F15)
|
||||
|
||||
Vor der nächsten Kampagne. Der Versand erreicht heute nur die ersten 500 und verschickt 480 davon doppelt.
|
||||
|
||||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||||
|---|---|---|---|
|
||||
| 2.1 | `CampaignSender.php:39`: Empfängerquery vor `chunkById` mit `reorder()` auf reine ID-Reihenfolge stellen. Die Sortierung ist eine UI-Sortierung der Vorschau, keine Versandreihenfolge. Test: 1.001 Nutzer mit gemischten `created_at`, jede ID genau einmal in den Empfängerzeilen. | S | E4 |
|
||||
| 2.2 | Unique-Index `(newsletter_campaign_id, user_id)` auf `newsletter_campaign_recipients`. Tabelle ist klein, kein FULLTEXT, ALTER ist sofort. | S | 2.1 |
|
||||
| 2.3 | Kampagnenstart atomar: `UPDATE … SET status='sending' WHERE id=? AND status='draft'`, nur bei einer betroffenen Zeile weitermachen. Empfänger mit `insertOrIgnore`. Test: zwei Instanzen derselben Kampagne, nur eine startet. | S | 2.2 |
|
||||
| 2.4 | Versandjob: atomarer Claim (`UPDATE … SET status='sending' WHERE id=? AND status='pending'`), Abschluss von Empfängerstatus und Zähler in einer Transaktion. `$tries` und `$backoff` am Job setzen statt am Worker. | M | keine |
|
||||
| 2.5 | Rückgabewert von `Mail::send()` auswerten: Bei Abbruch durch die Sperrliste Empfänger auf `suppressed` statt `sent`, Zähler nicht erhöhen. Neuer Status-Wert im Enum. Test: gesperrte Adresse endet nicht als „gesendet“. | S | 2.4 |
|
||||
| 2.6 | Befehl `newsletter:resume {campaign}`: alle `pending`-Empfänger einer `sending`-Kampagne neu einplanen, `failed` optional zurücksetzen. Ersetzt den unwirksamen `queue:retry`-Hinweis des Circuit Breakers. | S | 2.4 |
|
||||
|
||||
**Entscheidung**
|
||||
|
||||
- **E4** Versandreihenfolge: keine fachliche Bedeutung (empfohlen, dann reicht 2.1) oder „neueste zuerst“ als Anforderung (dann Cursor über `created_at, id`, Aufwand M).
|
||||
|
||||
**Abnahme:** Test über die 500er-Grenze in `NewsletterCampaignTest`; Doppelstart erzeugt keine zweite Empfängerzeile; gesperrte Adressen erscheinen in der Statistik als gesperrt.
|
||||
|
||||
---
|
||||
|
||||
## Block 3 — Sichtbarkeit, Löschung, Cache, Eingaben (F04, F13, F22, F12, F11, F23)
|
||||
|
||||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||||
|---|---|---|---|
|
||||
| 3.1 | `LOWER(email)` entfernen in `ContactAccessService.php:132,146`, `User.php:186`, `ImportNewsletterBounces.php:131,174`. B-Tree-Index auf `contacts.email` (6 MB, FULLTEXT vorhanden, Rebuild dauert Sekunden). Test: Groß-/Kleinschreibung und Leerzeichen bleiben korrekt; EXPLAIN auf dem Dev-Dump zeigt `ref`. | S | keine |
|
||||
| 3.2 | `ReleaseDetailPageData.php:76-80` und `press-kits/show.blade.php:273-306`: `withoutGlobalScopes()` durch `withoutGlobalScope(PortalScope::class)` ersetzen. Test: gelöschter Kontakt erscheint weder öffentlich noch im Pressefach. | S | keine |
|
||||
| 3.3 | Dasselbe für API v1 (`PressReleaseController.php:36,238,257`, `CompanyController.php:17,34`), Vorschau (`PressReleasePreviewController.php:33-42`), Medien-Controller und `PressRelease.php:194,207,219,430,440,730`. Den Test `PressReleasePreviewTest.php:237-247` auf `delete()` umstellen. Test je Modell: PM, Firma, Kontakt. | M | 3.2 |
|
||||
| 3.4 | Firmen-Observer oder Aufruf in `companies/edit.blade.php`: `WebCaches`-Flush bei `delete()` und bei Wechsel von `is_active`. Kontakt-Speichern und -Löschen im Pressefach flushen den Newsroom. `NewsroomDetailPageData.php:41` prüft vor dem Cache-Treffer auch `is_active` und `deleted_at`. Test: Cache warm, Firma deaktivieren, nächster Aufruf 404. `GoneTombstoneTest.php:162` so umbauen, dass der Cache vorher warm ist. | S | keine |
|
||||
| 3.5 | Gemeinsamer URL-Resolver für Meldeverfahren (`PressReleaseReportController.php:325`, `ReportFormController.php:102`): Host gegen `config/domains.php` prüfen, Portal und Sprache aus Host und Pfad, dann Slug. Bei Duplikaten die kanonische PM bevorzugen (`canonical_press_release_id is null desc`, wie in `RedirectPortalOnlyPagesFromHub.php:73`). Fremder Host: kein Treffer. Test: gleicher Slug in beiden Portalen und beiden Sprachen. | M | keine |
|
||||
| 3.6 | API v1: `per_page` als `integer|min:1|max:100`, `status` als `in:` über das Enum, `language` als `in:de,en`, `category_id` zusätzlich `is_active` und Portal-Match. Test: `per_page=-1`, `status[]=x` liefern 422. | S | keine |
|
||||
| 3.7 | `PressReleaseAttachmentStorage.php:66` und `ImageService.php:85`: Rückgabewert von `put()` prüfen, bei `false` Exception. Logo-Tausch in `press-kits/show.blade.php:153` und `companies/edit.blade.php:157`: erst speichern, dann DB, dann altes Logo löschen. Test mit gemocktem Storage, der `false` liefert. | S | keine |
|
||||
|
||||
**Abnahme:** Kein `withoutGlobalScopes()` ohne Argument mehr außerhalb von Admin-Verwaltung und Importern (Architekturtest in Pest, der die Trefferliste einfriert). Login-Pfad ohne Vollscan.
|
||||
|
||||
---
|
||||
|
||||
## Block 4 — KI-Prüfung und Scheduler (F07, F18, F20, F17, F19)
|
||||
|
||||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||||
|---|---|---|---|
|
||||
| 4.1 | `ClassifyPressRelease` und `ScorePressRelease` bekommen den `content_hash` zum Zeitpunkt der Einplanung im Konstruktor. Vor dem Speichern Vergleich mit dem aktuellen Hash; bei Abweichung Ergebnis verwerfen, ins `ki_audits` protokollieren, neuen Lauf einplanen. Hash ebenfalls in `ki_audits` speichern. | M | keine |
|
||||
| 4.2 | `PressReleaseService::publish()` und `PublishScheduledPressReleases` prüfen, dass `classified_at` nach der letzten Inhaltsänderung liegt oder der Audit-Hash zum aktuellen passt; sonst nicht veröffentlichen, sondern neu prüfen. Wenn der Re-Check einer terminierten PM rot wird, Kunde und Admin benachrichtigen statt still überspringen. Test: Text während der Analyse ändern, altes Ergebnis darf nicht freigeben. | M | 4.1 |
|
||||
| 4.3 | Kontingent periodisieren: Spalte `users.press_release_quota_period` (`YYYY-MM`). `consumePublishSlot()` und `pressReleaseQuotaRemaining()` setzen den Zähler auf null, wenn die Periode nicht dem aktuellen Monat entspricht. Der Cron wird zum Aufräumer, nicht zur einzigen Wahrheit. Test: Periode wechselt ohne Cron, doppelter Reset löscht nichts. | M | E5 |
|
||||
| 4.4 | `PublishScheduledPressReleases.php:117`: `FAILURE`, wenn `$failed > 0`. Fachliche Ablehnungen (Blacklist, rot) bleiben `SUCCESS`. Test auf Exit-Code. | S | keine |
|
||||
| 4.5 | `ScorePressRelease`: `public int $timeout = 120`. `retry_after` in `config/queue.php` auf 150. Runbook ergänzen. | S | keine |
|
||||
| 4.6 | `AggregatePressReleaseViews implements Isolatable`, Schedule mit `--isolated`. | S | keine |
|
||||
|
||||
**Entscheidung**
|
||||
|
||||
- **E5** Kontingent bleibt an den Kalendermonat gebunden (empfohlen, entspricht dem heutigen Verhalten) oder wechselt auf die Stripe-Abo-Periode. Nur Ersteres ist in diesem Plan.
|
||||
|
||||
---
|
||||
|
||||
## Block 5 — MAN-Abrechnung (F06, F16)
|
||||
|
||||
Beide latent. Aufnehmen, weil die Fixes klein sind und der Fehler beim ersten Datensatz mit Vertragsende ohne Vorwarnung eintritt.
|
||||
|
||||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||||
|---|---|---|---|
|
||||
| 5.1 | `ManualInvoiceService::duePaymentOptions()` und die Sperr-Nachprüfung berücksichtigen `grandfathered_until`: keine Rechnung für eine Periode, die nach dem Vertragsende beginnt. Test: gestern beendet, heute endend, unbefristet. | S | E6 |
|
||||
| 5.2 | Ablauf-Befehl im Scheduler: Vereinbarungen mit überschrittenem `grandfathered_until` auf beendet setzen und `GrandfatheringTerminated` senden. Heute passiert das nur manuell. | S | 5.1 |
|
||||
| 5.3 | `GenerateManualInvoices`: nicht abrechenbare Vereinbarungen aus dem Fenster ausschließen (Zustandsmarke `billing_hold_reason` an der Vereinbarung) und in Seiten weiterlaufen, bis kein abrechenbarer Kandidat mehr da ist. Die Admin-Zahlungsseite zeigt die Hold-Fälle bereits; nur die Marke fehlt. Test: 50 unvollständige vor einer vollständigen. | M | keine |
|
||||
|
||||
**Entscheidung**
|
||||
|
||||
- **E6** Verhalten am Vertragsende: letzte Teilperiode wird noch abgerechnet oder nicht. Empfehlung: keine Rechnung mehr, sobald `current_period_end` nach `grandfathered_until` liegt.
|
||||
|
||||
---
|
||||
|
||||
## Block 6 — Entwicklung und Qualitätsgates (F24, F27, F28, F26, F25)
|
||||
|
||||
| Nr. | Arbeitspaket | Aufwand | Abhängigkeit |
|
||||
|---|---|---|---|
|
||||
| 6.1 | `lint.yml`: `vendor/bin/pint --test` statt Schreibmodus, `contents: write` entfernen, auskommentierten Commit-Schritt löschen. | S | keine |
|
||||
| 6.2 | `composer.json` Script `dev`: `npm run dev:all` statt `npm run dev`. | S | keine |
|
||||
| 6.3 | `docs/README.md`: Stand-Absatz und toten Link auf `dev/archive/frontend/hub-flux/PROGRESS.md` korrigieren, Hinweis „historisch, aktueller Einstieg ist `dev/README.md`“. Kein Umbau der Doku. | S | keine |
|
||||
| 6.4 | Login-Modal in `site-header.blade.php:190`: Fokus beim Öffnen auf das erste Element, Rückgabe beim Schließen, Tab-Begrenzung. Ohne neue Abhängigkeit als kleine Alpine-Direktive; alternativ `@alpinejs/focus` (Abhängigkeit, Freigabe nötig). | S | keine |
|
||||
| 6.5 | Architekturtest: `withoutGlobalScopes()` ohne Argument nur in einer Whitelist erlaubt (schließt Block 3 ab). | S | 3.3 |
|
||||
| 6.6 | Aufgeschoben: MySQL-/Worker-Integrationstests als zweite CI-Stufe. Sinnvoll erst nach Block 2 und 4, wenn es konkrete Sperr- und Timeout-Fälle zu beweisen gibt. | L | 2, 4 |
|
||||
|
||||
---
|
||||
|
||||
## Nicht aufgenommen
|
||||
|
||||
- **F21 Legacy-Import.** Befund korrekt (Vollladen der Pivot-Tabellen, kein Transaktionsschutz, Wiederanlauf ändert UUID und Slug). Der Importer läuft seit dem 16.07. nicht mehr und ist nicht im Scheduler. Falls das Altsystem noch einmal einen Delta-Lauf braucht: `importRow()` in `DB::transaction` und vorhandene PM über `(legacy_portal, legacy_id)` statt über die Importmap erkennen. Bis dahin steht nur diese Notiz.
|
||||
- **Audit Abschnitt 10** (CSP, Proxy, Bildpixelgrenzen, Retention, PDF, Queue-Priorisierung). Nicht geprüft, nicht belegt. Einzelne Punkte können nach Block 6 als eigene Prüfaufträge folgen.
|
||||
- **Große Volt-Dateien zerlegen.** Richtig, aber kein Fehler. Läuft nebenbei, wenn ein Block ohnehin in `press-kits/show` oder `press-releases/show` arbeitet.
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge und Deploys
|
||||
|
||||
| Schritt | Inhalt | Deploy |
|
||||
|---|---|---|
|
||||
| 1 | Block 1, Punkte 1.1, 1.2, 1.6, 1.8, 1.9, 1.10 (alles S, kein Schema) | Deploy A, Hotfix-Charakter |
|
||||
| 2 | Block 2 komplett (Migration: ein Unique-Index auf kleiner Tabelle) | Deploy B, vor der nächsten Kampagne |
|
||||
| 3 | Block 1, Punkte 1.3, 1.4, 1.5, 1.7 (Migration `granted_via`, Bestandsprüfung) | Deploy C |
|
||||
| 4 | Block 3 | Deploy D (Migration: Index `contacts.email`) |
|
||||
| 5 | Block 4 | Deploy E (Migration: `quota_period`, `ki_audits.content_hash`) |
|
||||
| 6 | Block 5, Block 6 | Deploy F |
|
||||
|
||||
Migrationen in diesem Plan berühren `press_releases` nicht. `press_release_search` bleibt unangetastet. Für `users` (25 MB, FULLTEXT) ist die Spalte `quota_period` ein `ALGORITHM=COPY` von wenigen Sekunden; nach dem üblichen Fenster nachts einspielen.
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
Am 18.09.2026 alle sechs nach Empfehlung entschieden. Umsetzung ab demselben Tag.
|
||||
|
||||
| Nr. | Frage | Entscheidung |
|
||||
|---|---|---|
|
||||
| E1 | Fortify-Profiländerung abschalten oder umbiegen | abschalten |
|
||||
| E2 | Auto-Rechte bei Wegfall der Grundlage senken oder entfernen | auf `member` senken |
|
||||
| E3 | Nachweis vor Sicherheitsaktionen | TOTP- oder Recovery-Code |
|
||||
| E4 | Newsletter-Versandreihenfolge fachlich relevant | nein |
|
||||
| E5 | Kontingentperiode | Kalendermonat |
|
||||
| E6 | Letzte MAN-Periode am Vertragsende | keine Rechnung mehr |
|
||||
|
||||
## Umsetzungsprotokoll
|
||||
|
||||
| Datum | Punkte | Commit | Bemerkung |
|
||||
|---|---|---|---|
|
||||
452
dev/2026-09-11-audit/bildkennzeichnung-2026-09-15.md
Normal file
452
dev/2026-09-11-audit/bildkennzeichnung-2026-09-15.md
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
# Umsetzung: Bildnachweis, KI-Kennzeichnung und Rechtebestätigung
|
||||
|
||||
*Decision-Update und Umsetzungsspezifikation für pressekonto.com / presseecho.de / businessportal24.com*
|
||||
*Stand: 15.09.2026 · Status: Entwurf zur Umsetzung · Rechtstexte sind Entwürfe, Anwaltsprüfung offen*
|
||||
|
||||
---
|
||||
|
||||
## 0. Zweck
|
||||
|
||||
Bilder in Pressemitteilungen sollen mit weniger Formularaufwand eingereicht werden, dabei aber so dokumentiert sein, dass im Fall einer Abmahnung der Regress gegen den Einreicher trägt und der Bildnachweis öffentlich sichtbar ist. Das Portal liest Herkunftsdaten (Content Credentials, IPTC) beim Upload aus, belegt das Formular vor und sperrt eine Abschwächung der Kennzeichnung. Jede Veröffentlichung endet mit einer aktiven, protokollierten Zustimmung.
|
||||
|
||||
**Nicht Teil dieser Umsetzung**
|
||||
|
||||
- Der Bestand (migrierte und bisher veröffentlichte Bilder) wird nicht nachgeprüft und nicht nachbestätigt. Stichprobe ohne Auffälligkeiten, Bestand bleibt wie er ist.
|
||||
- Keine inhaltliche Bildprüfung durch das Portal (kein Vorab-Check auf Personen, Marken, Urheberschaft). Das Portal prüft Metadaten, nicht Inhalte.
|
||||
- Keine Änderung am Freigabelauf für Texte.
|
||||
|
||||
---
|
||||
|
||||
## 1. Bestätigte Entscheidungen
|
||||
|
||||
| Nr. | Entscheidung | Typ |
|
||||
|---|---|---|
|
||||
| E1 | Bestand bleibt unverändert. Neue Regeln gelten ab Deployment für neue Uploads. | Geschäft |
|
||||
| E2 | Bildnachweis wird sichtbar am Bild ausgegeben: dauerhaft als Textzeile unter dem Bild **und** als Overlay-Element im Bild, das sich zu einem Detailfenster öffnet. | Geschäft |
|
||||
| E3 | Beim Upload werden C2PA-Manifest und IPTC/XMP-Metadaten ausgelesen. Ergebnis wird persistiert, nicht nur im Bild belassen. | Technik |
|
||||
| E4 | Treffer in den Herkunftsdaten belegen das Formular vor. Die KI-Kennzeichnung ist dann **gesperrt** und kann nicht abgeschwächt werden. Eine Verschärfung durch den User bleibt immer möglich. | Geschäft |
|
||||
| E5 | Ohne Treffer muss der User Bildnachweis und KI-Kennzeichnung selbst angeben (Pflicht). | Geschäft |
|
||||
| E6 | In jedem Fall ist eine aktive Zustimmung (Toggle) mit protokollierter Textversion erforderlich. Kein stilles Durchwinken, auch nicht bei Vorbelegung. | Geschäft |
|
||||
| E7 | Die Abfragen zu Personen, Marken und privaten Orten entfallen als eigene Felder. Sie sind im Bestätigungstext und in den AGB abgedeckt. | Geschäft |
|
||||
| E8 | Die API erhält dieselben Bildfelder (Bildnachweis, KI-Kennzeichnung, Rechtebestätigung). Serverseitig läuft dieselbe Metadatenprüfung. connektar liefert derzeit keine Bilder, künftige API-Nutzer schon. | Technik |
|
||||
| E9 | Das Original der hochgeladenen Datei wird privat aufbewahrt (Beweissicherung). Öffentlich ausgeliefert wird nur das bearbeitete Derivat. | Technik |
|
||||
|
||||
**Grundregel für die Sperre (E4):** Die Sperre wirkt nur in eine Richtung. Metadaten „KI-generiert" → User kann nicht „Originalfoto" wählen. Metadaten „Digitalaufnahme" → User kann trotzdem „KI-bearbeitet" wählen. Damit gibt es keinen Fall, in dem das Portal einen User zwingt, eine Kennzeichnung wegzulassen.
|
||||
|
||||
---
|
||||
|
||||
## 2. Datenmodell
|
||||
|
||||
### 2.1 Tabelle `press_release_images` (Ergänzungen)
|
||||
|
||||
| Spalte | Typ | Inhalt |
|
||||
|---|---|---|
|
||||
| `credit` | string(255), not null | Bildnachweis, wie er ausgegeben wird (z. B. „Musterfirma GmbH", „Foto: Anna Beispiel / Musterfirma") |
|
||||
| `credit_source` | enum: `user`, `metadata`, `api` | Woher der Bildnachweis stammt |
|
||||
| `license_url` | string, nullable | Optionaler Link zur Lizenz oder Quelle |
|
||||
| `ai_declaration` | enum: `not_declared`, `original`, `ai_generated`, `ai_edited` | Effektive KI-Kennzeichnung |
|
||||
| `ai_declaration_source` | enum: `user`, `metadata`, `api`, `legacy` | Woher die Kennzeichnung stammt |
|
||||
| `ai_declaration_locked` | boolean, default false | true, wenn aus verifizierten Metadaten abgeleitet |
|
||||
| `provenance` | json, nullable | Normalisiertes Ergebnis der Metadatenprüfung (siehe 2.3) |
|
||||
| `provenance_checked_at` | timestamp, nullable | Zeitpunkt der Prüfung |
|
||||
| `provenance_tool_version` | string, nullable | z. B. `c2patool 0.x / exiftool 13.x` |
|
||||
| `original_path` | string, nullable | Pfad auf privater Disk |
|
||||
| `original_sha256` | string(64) | Hash der Originaldatei |
|
||||
| `rights_confirmation_id` | FK → `image_rights_confirmations` | Letzte gültige Bestätigung |
|
||||
|
||||
Bestand: alle vorhandenen Datensätze erhalten `ai_declaration = not_declared`, `ai_declaration_source = legacy`, `credit` bleibt wie migriert (auch leer).
|
||||
|
||||
### 2.2 Tabelle `image_rights_confirmations` (neu)
|
||||
|
||||
Das ist der eigentliche Beweis. Ohne diese Tabelle ist der Toggle im Streitfall wertlos.
|
||||
|
||||
| Spalte | Inhalt |
|
||||
|---|---|
|
||||
| `id` | |
|
||||
| `press_release_image_id` | FK |
|
||||
| `confirmed_at` | Zeitstempel |
|
||||
| `actor_type` | `user`, `magic_link`, `api_key` |
|
||||
| `actor_id` | User-ID, Token-ID oder API-Key-ID |
|
||||
| `company_id` | Firmenprofil, für das eingereicht wurde |
|
||||
| `confirmation_text_version` | Version des Bestätigungstextes (Single-Source aus `legal_document_sections`) |
|
||||
| `agb_version` | AGB-Version zum Zeitpunkt der Bestätigung |
|
||||
| `declaration_snapshot` | json: `credit`, `ai_declaration`, `ai_declaration_source`, `locked`, `provenance` zum Zeitpunkt der Bestätigung |
|
||||
| `file_sha256` | Hash der Originaldatei |
|
||||
| `ip_hash` | gehashte IP (kein Klartext) |
|
||||
| `user_agent` | gekürzt |
|
||||
|
||||
Regeln: Insert-only, keine Updates, keine Löschung außerhalb der Löschfrist. Bei erneuter Bestätigung (z. B. nach Bildtausch) neuer Datensatz.
|
||||
|
||||
### 2.3 Struktur `provenance` (json)
|
||||
|
||||
```json
|
||||
{
|
||||
"c2pa": {
|
||||
"present": true,
|
||||
"signature_valid": true,
|
||||
"issuer": "OpenAI OpCo, LLC",
|
||||
"claim_generator": "OpenAI Media Service API",
|
||||
"signed_at": "2026-09-09T10:12:00Z",
|
||||
"digital_source_type": "trainedAlgorithmicMedia",
|
||||
"actions": ["c2pa.created"]
|
||||
},
|
||||
"iptc": {
|
||||
"digital_source_type": null,
|
||||
"creator": null,
|
||||
"credit": null,
|
||||
"copyright_notice": null
|
||||
},
|
||||
"derived": {
|
||||
"ai_declaration": "ai_generated",
|
||||
"locked": true,
|
||||
"credit_suggestion": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Upload-Prüfung (Herkunftsdaten)
|
||||
|
||||
### 3.1 Ablauf
|
||||
|
||||
1. Datei landet über Livewire im Temp-Verzeichnis.
|
||||
2. Synchron (Timeout 10 s) laufen `c2patool` und `exiftool` gegen die Datei. Beides ist in unter einer Sekunde durch.
|
||||
3. Ergebnis wird normalisiert (2.3) und an das Formular zurückgegeben.
|
||||
4. Erst bei Speichern: Original auf private Disk, Derivate erzeugen, Datensatz anlegen.
|
||||
5. Bei Toolfehler oder Timeout: Formular im Zustand „kein Treffer" mit internem Vermerk `provenance_check_failed`. Der Upload wird nicht blockiert.
|
||||
|
||||
### 3.2 Werkzeuge
|
||||
|
||||
| Tool | Zweck | Installation |
|
||||
|---|---|---|
|
||||
| `exiftool` | IPTC/XMP/EXIF lesen und schreiben | `apt install libimage-exiftool-perl` |
|
||||
| `c2patool` | C2PA-Manifest lesen und Signatur prüfen | Release-Binary von GitHub (contentauth/c2patool), nach `/usr/local/bin` |
|
||||
|
||||
Aufruf aus Laravel über `Symfony\Component\Process\Process`, Ausgabe als JSON (`c2patool --detailed file.jpg`, `exiftool -json -struct file.jpg`).
|
||||
|
||||
### 3.3 Zuordnungsregeln
|
||||
|
||||
| Quelle | Wert | Ergebnis `ai_declaration` | Sperre |
|
||||
|---|---|---|---|
|
||||
| C2PA, Signatur gültig | `digitalSourceType` = `trainedAlgorithmicMedia` | `ai_generated` | ja |
|
||||
| C2PA, Signatur gültig | `compositeWithTrainedAlgorithmicMedia` | `ai_edited` | ja |
|
||||
| C2PA, Signatur gültig | `algorithmicallyEnhanced` | `ai_edited` | ja (siehe offener Punkt O3) |
|
||||
| C2PA, Signatur gültig | `digitalCapture`, `negativeFilm`, `positiveFilm`, `print` | Vorbelegung `original` | nein |
|
||||
| C2PA, Signatur **ungültig** oder nicht prüfbar | beliebig | Hinweis im Formular, keine Vorbelegung | nein |
|
||||
| IPTC/XMP `DigitalSourceType` (ohne C2PA) | `trainedAlgorithmicMedia` | `ai_generated` | ja |
|
||||
| IPTC/XMP `DigitalSourceType` (ohne C2PA) | `compositeWithTrainedAlgorithmicMedia` | `ai_edited` | ja |
|
||||
| IPTC/XMP `DigitalSourceType` (ohne C2PA) | `digitalCapture` u. a. | Vorbelegung `original` | nein |
|
||||
| keine der obigen | | `not_declared`, User muss wählen | nein |
|
||||
|
||||
Bildnachweis-Vorbelegung (immer editierbar): Reihenfolge `IPTC Credit` → `XMP dc:creator` → `IPTC By-line` → `EXIF Artist` → `CopyrightNotice`. Kein Treffer → Vorbelegung mit dem Firmennamen des Firmenprofils.
|
||||
|
||||
Nicht ausgewertet (zu schwach, zu viele Fehltreffer): `CreatorTool`-Strings, Dateinamen, Bildinhalt.
|
||||
|
||||
### 3.4 Was mit den Derivaten passiert
|
||||
|
||||
Skalierung und WebP-Konvertierung entfernen C2PA-Manifest und die meisten Metadaten. Deshalb:
|
||||
|
||||
- Ergebnis der Prüfung liegt in der Datenbank (2.3), nicht im Bild.
|
||||
- In das öffentliche Derivat werden per `exiftool` zurückgeschrieben: `XMP-dc:Rights` und `IPTC:CopyrightNotice` (= `credit`), `XMP-iptcExt:DigitalSourceType` (bei `ai_generated` / `ai_edited`). Damit tragen Google Bildersuche, LinkedIn und andere Plattformen den Nachweis und die Kennzeichnung weiter, statt dass das Portal sie entfernt. **BALD** (Phase 2).
|
||||
- EXIF-GPS und Geräteseriennummern werden im Derivat entfernt (Datenminimierung). Das Original behält alles, ist aber privat.
|
||||
|
||||
---
|
||||
|
||||
## 4. Formular v2
|
||||
|
||||
Drei Blöcke statt vier. Feld-Reihenfolge und Texte je nach Zustand.
|
||||
|
||||
### 4.1 Zustand A: Herkunftsdaten erkannt und gesperrt
|
||||
|
||||
```
|
||||
1 BILD
|
||||
[Datei] [Vorschau]
|
||||
|
||||
Herkunftsdaten erkannt
|
||||
Diese Datei enthält Content Credentials.
|
||||
Erstellt mit: OpenAI Media Service API
|
||||
Ausgestellt von: OpenAI OpCo, LLC am 9. September 2026
|
||||
Das Bild wird öffentlich als KI-generiert gekennzeichnet.
|
||||
Diese Kennzeichnung ist fest hinterlegt und kann nicht geändert werden.
|
||||
Wenn die Herkunftsdaten nicht zutreffen, melde dich bei uns: [Kontakt]
|
||||
|
||||
2 BILDNACHWEIS Pflicht
|
||||
[Musterfirma GmbH ] ← vorbelegt, editierbar
|
||||
So erscheint der Nachweis unter dem Bild. Bei Stock- oder Agenturbildern
|
||||
den Nachweis so eintragen, wie die Lizenz ihn verlangt.
|
||||
|
||||
Lizenz oder Quelle (optional) [https://...]
|
||||
|
||||
> Optionale Angaben Alt-Text, interne Hinweise
|
||||
|
||||
3 BESTÄTIGUNG
|
||||
[○ ] Ich bestätige, dass ich zur Veröffentlichung dieses Bildes
|
||||
berechtigt bin und meine Angaben zutreffen. Details ▾
|
||||
```
|
||||
|
||||
„Details ▾" klappt den vollständigen Bestätigungstext (5.1) auf. Der Toggle ist erst aktiv, wenn `credit` gefüllt ist.
|
||||
|
||||
### 4.2 Zustand B: keine Herkunftsdaten
|
||||
|
||||
```
|
||||
1 BILD
|
||||
[Datei] [Vorschau]
|
||||
|
||||
Keine Herkunftsdaten in der Datei gefunden. Bitte mach die Angaben selbst.
|
||||
|
||||
2 BILDNACHWEIS Pflicht
|
||||
[Musterfirma GmbH ] ← Firmenname vorbelegt
|
||||
Lizenz oder Quelle (optional) [https://...]
|
||||
|
||||
KI-KENNZEICHNUNG Pflicht
|
||||
( ) Originalfoto, nicht mit KI erzeugt oder bearbeitet
|
||||
( ) KI-generiert
|
||||
( ) KI-bearbeitet (z. B. generative Füllung, KI-Retusche)
|
||||
KI-generierte oder KI-bearbeitete Bilder erhalten öffentlich einen
|
||||
neutralen Hinweis. Übliche Bearbeitungen wie Zuschnitt, Farbkorrektur
|
||||
oder Schärfung gelten nicht als KI-Bearbeitung.
|
||||
|
||||
> Optionale Angaben
|
||||
|
||||
3 BESTÄTIGUNG
|
||||
[○ ] Ich bestätige ... Details ▾
|
||||
```
|
||||
|
||||
### 4.3 Zustand C: Vorbelegung ohne Sperre (z. B. C2PA `digitalCapture`)
|
||||
|
||||
Wie B, aber Radio „Originalfoto" vorausgewählt mit dem Hinweis „Vorbelegt aus den Herkunftsdaten der Datei (Aufnahme mit [Gerät/App]). Du kannst die Auswahl ändern."
|
||||
|
||||
### 4.4 Validierung
|
||||
|
||||
| Regel | Fehlermeldung |
|
||||
|---|---|
|
||||
| `credit` leer | „Bitte gib an, wer das Bild erstellt hat oder die Rechte hält." |
|
||||
| `ai_declaration` = `not_declared` bei Zustand B | „Bitte wähle aus, ob das Bild mit KI erstellt oder bearbeitet wurde." |
|
||||
| Toggle nicht aktiv | „Die Bestätigung ist erforderlich, um das Bild zu veröffentlichen." |
|
||||
| gesperrter Wert wird per Request manipuliert | serverseitig verwerfen, Metadatenwert bleibt, Log-Eintrag `lock_bypass_attempt` |
|
||||
|
||||
Serverseitig gilt immer: effektiver Wert = max(Metadaten, Userangabe) in der Reihenfolge `original` < `ai_edited` < `ai_generated`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Texte
|
||||
|
||||
Alle Texte gehören ins Single-Source-System (`legal_document_sections`) mit eigener Versionsnummer, damit `confirmation_text_version` referenzierbar ist. Entwürfe, vor Livegang durch den Anwalt prüfen lassen.
|
||||
|
||||
### 5.1 Bestätigungstext v2 (Upload, vollständig)
|
||||
|
||||
> Ich bestätige, dass ich zur Veröffentlichung dieses Bildes auf presseecho.de und businessportal24.com berechtigt bin. Das umfasst die Rechte des Urhebers, die Rechte abgebildeter Personen sowie Marken- und sonstige Rechte Dritter. Mein Bildnachweis und meine Angabe zur KI-Kennzeichnung sind zutreffend. Ich weiß, dass ich für fehlerhafte oder unvollständige Angaben verantwortlich bin und den Betreiber von Ansprüchen Dritter freistelle (Ziffer [X] der AGB).
|
||||
|
||||
Kurzform am Toggle: „Ich bestätige, dass ich zur Veröffentlichung dieses Bildes berechtigt bin und meine Angaben zutreffen."
|
||||
|
||||
### 5.2 AGB-Klausel (Entwurf, neue Ziffer „Bilder und Medieninhalte")
|
||||
|
||||
> **X.1 Rechte.** Der Kunde reicht nur Bilder ein, für die er über die zur Veröffentlichung auf den Portalen erforderlichen Rechte verfügt. Dazu gehören das Recht zur öffentlichen Zugänglichmachung, zur technisch erforderlichen Bearbeitung (insbesondere Skalierung, Zuschnitt auf Vorschauformate, Formatkonvertierung) und zur Weitergabe im Rahmen der Verbreitung der Pressemitteilung über Feeds und Partner.
|
||||
>
|
||||
> **X.2 Rechte Dritter.** Der Kunde stellt sicher, dass abgebildete Personen in die Veröffentlichung eingewilligt haben oder eine gesetzliche Ausnahme greift und dass keine Marken-, Eigentums- oder sonstigen Rechte Dritter verletzt werden.
|
||||
>
|
||||
> **X.3 Bildnachweis.** Der Kunde gibt zu jedem Bild einen Bildnachweis an. Der Betreiber gibt diesen Nachweis am Bild aus. Der Kunde ist dafür verantwortlich, dass der Nachweis den Anforderungen der jeweiligen Lizenz entspricht.
|
||||
>
|
||||
> **X.4 KI-Kennzeichnung.** Der Kunde kennzeichnet Bilder, die mit KI erzeugt oder bearbeitet wurden. Der Betreiber liest in der Datei enthaltene Herkunftsdaten (etwa Content Credentials) aus und kennzeichnet das Bild entsprechend. Eine aus solchen Herkunftsdaten abgeleitete Kennzeichnung kann der Kunde nicht abschwächen. Eigene Pflichten des Kunden aus der Verordnung (EU) 2024/1689 bleiben unberührt.
|
||||
>
|
||||
> **X.5 Freistellung.** Der Kunde stellt den Betreiber von allen Ansprüchen Dritter frei, die auf einer Verletzung der Pflichten nach X.1 bis X.4 beruhen, einschließlich der angemessenen Kosten der Rechtsverfolgung und Rechtsverteidigung. Der Betreiber informiert den Kunden unverzüglich über geltend gemachte Ansprüche und stimmt sich mit ihm über die Verteidigung ab.
|
||||
>
|
||||
> **X.6 Entfernung.** Der Betreiber darf Bilder bei begründeten Hinweisen auf eine Rechtsverletzung ohne vorherige Ankündigung entfernen. Das Meldeverfahren des Betreibers gilt entsprechend. Eine Entfernung nach diesem Absatz begründet keinen Anspruch des Kunden auf Erstattung oder Gutschrift.
|
||||
>
|
||||
> **X.7 Beweissicherung.** Der Betreiber speichert die eingereichte Originaldatei sowie die Angaben und die Bestätigung des Kunden für die Dauer von [Frist] nach Ende der Veröffentlichung.
|
||||
|
||||
### 5.3 Ergänzung Datenschutzerklärung
|
||||
|
||||
Neuer Absatz unter Verarbeitung bei Veröffentlichung: Speicherung der Originaldatei inklusive enthaltener Metadaten, der Bestätigungsdaten (Zeitpunkt, gehashte IP, Textversion) für [Frist]; Rechtsgrundlage Art. 6 Abs. 1 lit. f (Nachweis- und Verteidigungsinteresse) und lit. c, soweit gesetzliche Aufbewahrung. Hinweis, dass öffentliche Bildderivate von Standort- und Gerätedaten bereinigt werden.
|
||||
|
||||
### 5.4 Ergänzung Veröffentlichungsrichtlinien
|
||||
|
||||
Kurzer Abschnitt „Bilder": Bildnachweis ist Pflicht, KI-Bilder werden gekennzeichnet, Herkunftsdaten werden ausgelesen, Bilder ohne Rechte werden entfernt. Kein Juristendeutsch, drei bis vier Sätze.
|
||||
|
||||
### 5.5 Texte im öffentlichen Detailfenster (Overlay)
|
||||
|
||||
Titel: **Bildnachweis**
|
||||
|
||||
| Zeile | Text | Bedingung |
|
||||
|---|---|---|
|
||||
| Nachweis | Bild: [credit] | immer |
|
||||
| Lizenz | Lizenz / Quelle: [Link] | wenn `license_url` |
|
||||
| KI-Hinweis | Dieses Bild wurde mit KI erstellt. | `ai_generated` |
|
||||
| KI-Hinweis | Dieses Bild wurde mit KI bearbeitet. | `ai_edited` |
|
||||
| Herkunft | Herkunftsnachweis in der Datei: erstellt mit [claim_generator], ausgestellt von [issuer] am [Datum]. Vom Portal beim Upload ausgelesen. | `provenance.c2pa.present && signature_valid` |
|
||||
| Verantwortlich | Verantwortlich für Bild und Angaben: [Firma] | immer |
|
||||
| Bestand | Bildnachweis vom Einreicher nicht hinterlegt. | `credit` leer (Bestand) |
|
||||
| Aktion | Bild melden → [Meldeverfahren] | immer |
|
||||
|
||||
Sichtbare Zeile unter dem Bild (ohne Klick): „Bild: [credit]" plus Zusatz „· KI-generiert" oder „· KI-bearbeitet".
|
||||
|
||||
Bewusst weggelassen: Wörter wie „Deepfake", „manipuliert", „Warnung". Der Hinweis ist neutral, keine Wertung.
|
||||
|
||||
---
|
||||
|
||||
## 6. Öffentliche Ausgabe
|
||||
|
||||
### 6.1 Overlay-Element
|
||||
|
||||
- Position: unten links im Bild, rundes Icon (≈ 28 px), halbtransparenter Hintergrund, Symbol „i" oder ein eigenes Zeichen. Kein „cr"-Logo verwenden, das ist die geschützte Marke der C2PA-Initiative und nur für konforme Anzeigen gedacht.
|
||||
- Klick / Tastatur (Enter, Escape) öffnet ein Popover mit den Zeilen aus 5.5.
|
||||
- Alpine.js, keine externe Bibliothek. `aria-haspopup`, `aria-expanded`, Fokusfalle im Popover.
|
||||
- Auf Mobil: Popover wird zum Bottom-Sheet.
|
||||
|
||||
### 6.2 Sichtbare Nachweiszeile
|
||||
|
||||
Immer unter dem Bild, auch ohne Klick. Grund: Die Urhebernennung nach § 13 UrhG muss „in üblicher Weise" erfolgen. Ein Nachweis, der nur hinter einem Klick liegt, ist angreifbar. Die Zeile ist die eigentliche Erfüllung, das Overlay ist die Erklärung.
|
||||
|
||||
### 6.3 Weitere Ausgabekanäle
|
||||
|
||||
| Kanal | Umsetzung | Phase |
|
||||
|---|---|---|
|
||||
| Artikelseite | Zeile + Overlay | 1 |
|
||||
| Listen / Teaser | kein Nachweis (Vorschau, Link zur Artikelseite) | 1 |
|
||||
| RSS / Feeds | `<media:content>` mit `<media:credit>` und `<media:description>` inkl. KI-Zusatz | 2 (BALD) |
|
||||
| Structured Data | `ImageObject` mit `creditText`, `copyrightNotice`, `creator`, optional `license`; bei KI zusätzlich `digitalSourceType` | 2 (BALD) |
|
||||
| Derivat-Metadaten | Rückschreiben per exiftool (3.4) | 2 (BALD) |
|
||||
| OpenGraph-Bild | keine Änderung | |
|
||||
|
||||
---
|
||||
|
||||
## 7. API
|
||||
|
||||
### 7.1 Request-Felder pro Bild
|
||||
|
||||
```json
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"file": "<base64 oder URL>",
|
||||
"credit": "Musterfirma GmbH",
|
||||
"license_url": null,
|
||||
"ai_declaration": "original",
|
||||
"alt_text": "…",
|
||||
"rights_confirmed": true,
|
||||
"rights_confirmation_text_version": "img-confirm-v2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Feld | Pflicht | Regel |
|
||||
|---|---|---|
|
||||
| `credit` | ja, wenn Bild vorhanden | max. 255 Zeichen |
|
||||
| `ai_declaration` | ja, wenn Bild vorhanden | `original`, `ai_generated`, `ai_edited` |
|
||||
| `rights_confirmed` | ja, muss `true` sein | |
|
||||
| `rights_confirmation_text_version` | ja | muss eine aktive Version sein, sonst 422 mit Hinweis auf die aktuelle |
|
||||
| `license_url`, `alt_text` | nein | |
|
||||
|
||||
### 7.2 Serverseitige Verarbeitung
|
||||
|
||||
1. Dieselbe Metadatenprüfung wie beim Upload (3.).
|
||||
2. Effektive Kennzeichnung = max(Metadaten, `ai_declaration`). Widerspricht die API-Angabe den Metadaten (API sagt `original`, Datei sagt `ai_generated`), wird der Metadatenwert gesetzt und im Response gemeldet. Kein Abbruch.
|
||||
3. `image_rights_confirmations` mit `actor_type = api_key`.
|
||||
4. Fehlt `credit` oder `rights_confirmed`: Bild wird verworfen, PM wird ohne Bild weiterverarbeitet, Response enthält Warnung. Die PM wird nicht blockiert (siehe O5).
|
||||
|
||||
### 7.3 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"id": "…",
|
||||
"credit": "Musterfirma GmbH",
|
||||
"ai_declaration_effective": "ai_generated",
|
||||
"ai_declaration_source": "metadata",
|
||||
"provenance_detected": true,
|
||||
"warnings": ["ai_declaration überschrieben: Datei enthält Content Credentials (OpenAI OpCo, LLC)."]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 API-Dokumentation (Textbaustein)
|
||||
|
||||
> Bilder sind optional. Wird ein Bild übermittelt, sind `credit`, `ai_declaration` und `rights_confirmed: true` Pflicht. Das Portal liest Herkunftsdaten in der Datei aus (C2PA, IPTC). Enthält die Datei eine KI-Kennzeichnung, gilt diese unabhängig von der übermittelten `ai_declaration`; eine Abschwächung ist nicht möglich. Bilder ohne die Pflichtfelder werden verworfen, die Pressemitteilung wird ohne Bild verarbeitet. Mit `rights_confirmed: true` bestätigt der API-Nutzer für jedes Bild die Rechteerklärung in der jeweils angegebenen Version. Die Rechteerklärung ist Bestandteil der API-Nutzungsbedingungen.
|
||||
|
||||
### 7.5 Vertragliche Seite
|
||||
|
||||
Die API-Nutzungsbedingungen (bzw. der Partnervertrag) müssen die Ziffer aus 5.2 enthalten oder darauf verweisen. Für connektar gilt das auch ohne Bilder, weil die Lücke sonst mit dem ersten übermittelten Bild aufgeht. Das ist Teil der ohnehin ausstehenden Syndikationsvereinbarung.
|
||||
|
||||
---
|
||||
|
||||
## 8. Beweissicherung und Aufbewahrung
|
||||
|
||||
| Was | Wo | Frist |
|
||||
|---|---|---|
|
||||
| Originaldatei | private Disk, nicht öffentlich erreichbar, Pfad in `original_path` | [Frist] nach Depublizierung, Vorschlag 3 Jahre (Verjährung § 102 UrhG / § 195 BGB), Anwalt bestätigt |
|
||||
| `image_rights_confirmations` | Datenbank, insert-only | wie Original |
|
||||
| `provenance` json | Datenbank | wie Datensatz |
|
||||
| Lock-Bypass-Versuche | Audit-Log ohne Klartext-Nutzdaten | 12 Monate |
|
||||
|
||||
Löschung des Originals erfolgt per Scheduler nach Frist, mit Log-Eintrag. Bei laufender Auseinandersetzung (Flag `legal_hold` am Datensatz) wird nicht gelöscht.
|
||||
|
||||
---
|
||||
|
||||
## 9. Umsetzungsreihenfolge
|
||||
|
||||
| Phase | Inhalt | Abhängigkeit |
|
||||
|---|---|---|
|
||||
| **1a** | Migration: neue Spalten, Tabelle `image_rights_confirmations`, Bestand auf `legacy` setzen | keine |
|
||||
| **1b** | `exiftool` und `c2patool` auf dem Server, Service `ImageProvenanceInspector` mit Zuordnungsregeln (3.3) und Tests gegen Beispieldateien (OpenAI, Adobe Firefly, Google, Kamera-JPEG, Screenshot) | 1a |
|
||||
| **1c** | Formular v2 (Zustände A/B/C), Validierung, Sperrlogik serverseitig, Bestätigungs-Insert | 1a, 1b |
|
||||
| **1d** | Original-Ablage auf private Disk, Hash, Derivat-Pipeline unverändert | 1a |
|
||||
| **1e** | Öffentliche Ausgabe: Nachweiszeile + Overlay auf Artikelseiten, Bestandsfallback | 1a |
|
||||
| **1f** | Texte ins Single-Source-System (5.1, 5.4, 5.5), Versionen anlegen | 1c |
|
||||
| **1g** | AGB-Entwurf (5.2) und DSE-Ergänzung (5.3) an Anwalt | unabhängig, blockiert Livegang von 1c |
|
||||
| **2a BALD** | API-Felder, Verarbeitung, Response, Doku (7.) | 1b, 1c |
|
||||
| **2b BALD** | Metadaten-Rückschreiben ins Derivat, Structured Data, Feeds (3.4, 6.3) | 1e |
|
||||
| **2c BALD** | Löschscheduler mit `legal_hold` (8.) | 1d, Anwaltsfrist |
|
||||
|
||||
Livegang von Phase 1 erst, wenn 1g (AGB-Klausel) geprüft ist. Bis dahin kann das Formular hinter einem Feature-Flag laufen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Offene Punkte nach Dringlichkeit
|
||||
|
||||
| Nr. | Punkt | Wer | Dringlichkeit |
|
||||
|---|---|---|---|
|
||||
| O1 | Aufbewahrungsfrist für Original und Bestätigung festlegen | Kevin + Anwalt | hoch, blockiert 1g und 5.3 |
|
||||
| O2 | Bestandsbilder ohne `credit`: Fallbacktext im Overlay wie in 5.5 („nicht hinterlegt") oder Firmenname des Einreichers als Nachweis? Firmenname ist eine Behauptung, die falsch sein kann. Empfehlung: „nicht hinterlegt" + Verantwortlich-Zeile. | Kevin | hoch, blockiert 1e |
|
||||
| O3 | `algorithmicallyEnhanced` (KI-Upscaling, KI-Entrauschen): als `ai_edited` sperren oder nur vorbelegen? Sperren ist konservativ, trifft aber keine üblichen Kamerafotos, weil Kameras den Wert nicht schreiben. Empfehlung: sperren. | Kevin | mittel |
|
||||
| O4 | Widerspruchsweg bei gesperrter Kennzeichnung: nur Kontaktlink (Empfehlung, kein Betreuungsaufwand im Regelfall) oder Begründungsfeld mit Admin-Freigabe? | Kevin | mittel |
|
||||
| O5 | API: Bild ohne Pflichtfelder verwerfen und PM durchlassen (Empfehlung) oder gesamte PM mit 422 ablehnen? | Kevin | mittel, vor 2a |
|
||||
| O6 | Textstil im Formular: Du oder Sie? Das Dokument nutzt Du für Formular und Overlay, Sie für AGB. An bestehende Portalkonvention angleichen. | Kevin | niedrig |
|
||||
| O7 | Eigenes Overlay-Icon gestalten (kein „cr"-Logo) | Kevin | niedrig |
|
||||
|
||||
---
|
||||
|
||||
## 11. Anwaltsfragen (Ergänzung zur Liste im Handover-Dokument)
|
||||
|
||||
5. Macht der Freigabelauf (inkl. KI-Scoring) die Bilder zu eigenen Inhalten im Sinne der marions-kochbuch-Rechtsprechung (BGH I ZR 166/07)? Reicht die Verantwortlich-Zeile am Bild und die neutrale Kommunikation der Freigabe als „formale Prüfung", um im Hoster-Status zu bleiben?
|
||||
6. Hält die Freistellungsklausel X.5 der AGB-Kontrolle nach § 307 BGB im B2B-Verkehr stand, insbesondere ohne Verschuldensbezug und mit Rechtsverteidigungskosten?
|
||||
7. Ist die gesperrte, aus Metadaten abgeleitete KI-Kennzeichnung (X.4) rechtlich unproblematisch, auch wenn die Herkunftsdaten im Einzelfall falsch sind und der Kunde das nicht ändern kann?
|
||||
8. Angemessene Aufbewahrungsfrist für Originaldatei und Bestätigung (O1). Rechtsgrundlage in der Datenschutzerklärung.
|
||||
9. Genügt die Nachweiszeile unter dem Bild der Nennung „in üblicher Weise" nach § 13 UrhG, oder muss der Nachweis ins Bild bzw. direkt daneben?
|
||||
10. Übertragung der Rechteerklärung auf API-Nutzer: reicht `rights_confirmed: true` pro Request in Verbindung mit den API-Nutzungsbedingungen, oder braucht es eine einmalige schriftliche Erklärung je Partner?
|
||||
|
||||
---
|
||||
|
||||
## 12. Revisit-Bedingungen
|
||||
|
||||
- connektar oder ein anderer API-Partner beginnt, Bilder zu liefern → Phase 2a wird sofort fällig.
|
||||
- Die Kommission veröffentlicht die finalen Leitlinien zu Art. 50 KI-VO → Zuordnungsregeln (3.3) und Overlay-Texte (5.5) gegenprüfen.
|
||||
- Erste Abmahnung wegen eines Bildes → Regressweg real testen, Lücken dokumentieren.
|
||||
- Mehr als eine Handvoll Widersprüche gegen gesperrte Kennzeichnungen pro Monat → O4 neu entscheiden.
|
||||
- Anteil KI-gekennzeichneter Bilder wird relevant → prüfen, ob der neutrale Hinweis in Listen/Teasern ergänzt wird.
|
||||
|
||||
---
|
||||
|
||||
## 13. Anti-Zombie-Check
|
||||
|
||||
| Kriterium | Ergebnis |
|
||||
|---|---|
|
||||
| Versteckte Kosten | keine. Bilder bleiben kostenlos, Kennzeichnung kostet nichts |
|
||||
| Automatik gegen den Nutzer | Sperre ist erklärt, wirkt nur gegen Abschwächung, Kontaktweg steht daneben |
|
||||
| Transparenz | Herkunftsdaten werden dem User im Formular genau so gezeigt wie später öffentlich |
|
||||
| Menschlicher Kontakt | Widerspruch gegen Sperre läuft über direkten Kontakt |
|
||||
| Betreuungsaufwand | Regelfall läuft ohne Admin. Einziger manueller Fall: Widerspruch gegen Sperre |
|
||||
| Formularaufwand | von 6 Pflichtangaben + Toggle auf 1 bis 2 Pflichtangaben + Toggle |
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
# Feedback / Audit: Bildnachweis, KI-Kennzeichnung und Rechtebestätigung
|
||||
|
||||
**Stand:** 15.09.2026
|
||||
|
||||
**Grundlage:** [Umsetzungsvorschlag vom 15.09.2026](bildkennzeichnung-2026-09-15.md)
|
||||
|
||||
**Codebasis:** Arbeitsverzeichnis `/var/www/html`, HEAD `d60f31d78e33033207896cd8ea070a54f44ca54d`, einschließlich des beim Audit vorhandenen Arbeitsstands.
|
||||
|
||||
**Prüfumfang:** Statischer Codeabgleich, lesende Prüfung der relevanten Datenbankstrukturen und bestehender Tests sowie Gegenprüfung technischer Standards anhand offizieller Quellen. Keine Implementierung.
|
||||
|
||||
## 1. Gesamturteil
|
||||
|
||||
**Die Richtung ist sinnvoll. Den Entwurf würde ich vor der Umsetzung an mehreren Stellen korrigieren.** Weniger Formularfelder, ein sichtbarer Bildnachweis, eine eigenständige KI-Kennzeichnung und eine nachvollziehbare Zustimmung passen zur Anwendung. Der bestehende Code bietet dafür bereits wichtige Grundlagen.
|
||||
|
||||
Die größten Probleme liegen in der Bestandsmigration, der Vertrauensbewertung von Metadaten und den Übergängen zwischen Upload, Veröffentlichung und späterer Löschung. Einige als vorhanden vorausgesetzte Bausteine fehlen; andere sind bereits anders umgesetzt.
|
||||
|
||||
**Die fünf wichtigsten Ergebnisse:**
|
||||
|
||||
1. **Bestehende KI-Deklarationen unbedingt erhalten.** Der vorgeschlagene pauschale Wechsel auf `not_declared` würde vorhandene Angaben überschreiben und öffentliche KI-Hinweise entfernen.
|
||||
2. **`algorithmicallyEnhanced` ist kein belastbarer KI-Nachweis.** Der IPTC-Wert umfasst ausdrücklich auch gewöhnliches Schärfen und Entrauschen.
|
||||
3. **Eine gültige C2PA-Signatur und frei editierbare IPTC-Angaben sind unterschiedliche Vertrauensstufen.** Die aktuelle Sperrregel vermischt sie.
|
||||
4. **Die Bild-API existiert bereits.** Sie muss zum ersten produktiven Einsatz der neuen Regeln berücksichtigt werden; das geplante JSON-Beispiel entspricht außerdem nicht dem heutigen Upload-Vertrag.
|
||||
5. **Originalablage, Zustimmung und Löschregeln müssen gemeinsam geplant werden.** Eine private Disk allein schützt das Original nicht vor öffentlicher Auslieferung über die vorhandene Medienroute.
|
||||
|
||||
Prioritäten in diesem Audit: **P1** = vor Umsetzung beziehungsweise Livegang verbindlich klären; **P2** = in die Umsetzungsspezifikation aufnehmen. Es wird damit kein bereits eingetretener Produktionsschaden behauptet.
|
||||
|
||||
## 2. Was bereits vorhanden ist
|
||||
|
||||
| Thema | Bestehender Code | Konsequenz für den Vorschlag |
|
||||
|---|---|---|
|
||||
| Öffentlich sichtbarer Bildnachweis | `copyright`, mit Fallback auf `author`; Ausgabe unter dem Artikelbild | Bestehende Ausgabe erweitern, Daten nicht unverbunden duplizieren |
|
||||
| KI-Kennzeichnung pro Bild | `AiImageDeclaration` mit `original`, `ai_generated`, `ai_edited`; öffentlicher Badge | Kein vollständiger Neubau erforderlich |
|
||||
| Rechteangaben beim Upload | Gemeinsamer Volt-Bildmanager für Kunden und Admins; `rights_confirmed_at` und Lizenzfelder | Vereinfachung ist möglich; bestehende Nachweise erhalten |
|
||||
| KI-Snapshot beim Einreichen | `PressReleaseService::logAiDeclaration()` und `press_release_ai_declarations` | Neue Bildrechtebestätigung mit diesem Ablauf abstimmen |
|
||||
| Private Bildspeicherung | Neue Pressebilder werden standardmäßig auf `local` gespeichert | Öffentliches Derivat und privates Beweisoriginal brauchen getrennte Zugriffspfade |
|
||||
| Bild-API | Eigene v1- und Legacy-Bilduploads | Neue Regeln müssen sämtliche aktiven Einstiegspunkte erfassen |
|
||||
| Rechtstextversionen | `legal_documents` mit Typ, Version, Gültigkeitsdatum und Notiz | Keine bereits vorhandene Tabelle `legal_document_sections`; Textarchivierung fehlt als eigener Baustein |
|
||||
| Weitere Darstellung | Kundenansicht, Adminansicht, öffentliche Vorschau und Beleg-PDF | Zusätzlich zur Artikelseite in den Änderungsumfang aufnehmen |
|
||||
|
||||
Codebelege: [Bildmodell](../../app/Models/PressReleaseImage.php), [KI-Enum](../../app/Enums/AiImageDeclaration.php), [Bildmanager](../../resources/views/livewire/components/press-release-images-manager.blade.php), [Artikelseite](../../resources/views/web/release-detail.blade.php), [Veröffentlichungsservice](../../app/Services/PressRelease/PressReleaseService.php), [Rechtstextmodell](../../app/Models/LegalDocument.php), [PDF-Ausgabe](../../app/Services/PressRelease/ProofPdfRenderer.php).
|
||||
|
||||
## 3. Priorisierte Befunde
|
||||
|
||||
### F01 · P1 · Die Bestandsmigration widerspricht dem Bestandsschutz
|
||||
|
||||
**Entwurf:** Abschnitt 2.1 setzt alle bestehenden Bilder auf `ai_declaration = not_declared`. E1 und Abschnitt 0 verlangen gleichzeitig, den Bestand unverändert zu lassen.
|
||||
|
||||
**Code:** Es gibt bereits ausdrücklich deklarierte KI-Bilder. Die Migration vom 08.07.2026 hat zusätzlich Bilder mit `is_ai_generated = true` auf `ai_generated` gesetzt. Die öffentliche Artikelseite erzeugt ihren KI-Hinweis aus `ai_declaration`.
|
||||
|
||||
**Auswirkung:** Bereits abgegebene Angaben gehen verloren. Bei bisher als `ai_edited` oder `ai_generated` gekennzeichneten Bildern kann der öffentliche Hinweis verschwinden. Ohne Erweiterung des PHP-Enums führt ein gespeichertes `not_declared` beim Enum-Cast außerdem zu einem Fehler. Nach einer Erweiterung würde die heutige Einreichungsprüfung den Wert dennoch nicht erkennen: Sie prüft nur auf `NULL`.
|
||||
|
||||
**Empfehlung:**
|
||||
|
||||
- Vorhandene Werte in `ai_declaration`, `copyright`, `author` und den bisherigen Rechtefeldern erhalten.
|
||||
- Nur die fehlende historische Herkunft als `legacy` beziehungsweise `unknown` ergänzen; nicht die Deklaration selbst ersetzen.
|
||||
- Für fehlende Deklarationen zunächst das vorhandene `NULL` weiterverwenden. Ein vierter Enum-Wert ist nur sinnvoll, wenn alle Leser, Validatoren und Veröffentlichungsprüfungen angepasst werden.
|
||||
- Bestandsschutz ausdrücklich für alte Entwürfe, abgelehnte PMs und erneutes Einreichen definieren. Ein neues Ersatzbild unterliegt immer den neuen Regeln.
|
||||
- Bestandsschutz über eine serverseitige Regelversion oder einen eindeutig definierten Stichtag feststellen, nicht über ein frei setzbares Request-Feld.
|
||||
|
||||
**Belege:** [Backfill](../../database/migrations/2026_07_08_172113_backfill_ai_declaration_on_press_release_images.php), [Enum](../../app/Enums/AiImageDeclaration.php), [Artikelseite, Zeilen 47–50](../../resources/views/web/release-detail.blade.php#L47), [Vollständigkeitsprüfung, ab Zeile 572](../../app/Services/PressRelease/PressReleaseService.php#L572).
|
||||
|
||||
### F02 · P1 · C2PA-Verifikation braucht mehr als `signature_valid`
|
||||
|
||||
**Entwurf:** Eine gültige Signatur löst die Sperre aus. Unsigned IPTC/XMP löst dieselbe Sperre aus; das Datenmodell beschreibt sie zugleich als Ergebnis „verifizierter Metadaten“.
|
||||
|
||||
Eine kryptografisch gültige Signatur allein beantwortet weder, ob dem Unterzeichner vertraut wird, noch ob alle relevanten Bindungen an die hochgeladene Datei gültig sind. Frei bearbeitbare IPTC/XMP-Felder besitzen diese Signaturprüfung überhaupt nicht. Die C2PA-Spezifikation unterscheidet die Prüfung von Manifest, Aussagen, Signatur, Dateibindung und Vertrauenskette. [Quelle: C2PA-Spezifikation, Validierung](https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html).
|
||||
|
||||
**Empfehlung:** Ein normalisiertes Ergebnis mit getrennten Zuständen vorsehen, etwa:
|
||||
|
||||
- `absent`: keine unterstützten Herkunftsdaten gefunden;
|
||||
- `validated_trusted`: relevante Prüfungen erfolgreich, Unterzeichner nach definierter Vertrauensliste akzeptiert;
|
||||
- `validated_untrusted`: technisch geprüft, Vertrauensanker nicht akzeptiert;
|
||||
- `invalid`: Prüfung fehlgeschlagen;
|
||||
- `unsupported` oder `error`: nicht unterstützt beziehungsweise technische Prüfung nicht abgeschlossen;
|
||||
- ergänzend `unsigned_metadata` für ungesicherte IPTC/XMP-Angaben.
|
||||
|
||||
Eine automatische Mindestkennzeichnung würde ich auf belastbare, vertrauenswürdig validierte Aussagen begrenzen. Bei ungesicherten Angaben würde ich vorbelegen und eine begründete Korrektur protokollieren. Soll auch dort geschäftlich zwingend gesperrt werden, muss das ausdrücklich als Portalregel formuliert werden; die Anzeige darf sie nicht als verifizierte Content Credentials ausgeben.
|
||||
|
||||
Zusätzlich fehlen Regeln für widersprüchliche C2PA-/XMP-Angaben, mehrere Manifeste, referenzierte Ausgangsbilder und KI-Bestandteile eines zusammengesetzten Bildes. Der `digitalSourceType` eines Bestandteils darf nicht blind als Typ des gesamten Endbilds übernommen werden. Diese Fälle mit echten Dateien und festgelegten Erwartungen prüfen. Prüfergebnis und verwendete Vertrauens-/Regelversion speichern.
|
||||
|
||||
### F03 · P1 · `algorithmicallyEnhanced` nicht automatisch als KI-Bearbeitung sperren
|
||||
|
||||
**Entwurf:** Abschnitt 3.3 und O3 schlagen `ai_edited` mit Sperre vor.
|
||||
|
||||
**Gegenprüfung:** IPTC beschreibt den Wert als algorithmische Änderung oder Korrektur ohne Veränderung des Hauptinhalts, ausdrücklich beispielsweise Schärfung und Rauschreduzierung. Generative KI ist dafür keine Voraussetzung. Für generative Ergänzungen wie Inpainting und Outpainting existiert `compositeWithTrainedAlgorithmicMedia`. [IPTC: algorithmicallyEnhanced](https://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced), [IPTC: compositeWithTrainedAlgorithmicMedia](https://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia).
|
||||
|
||||
Die Sperre widerspricht damit dem eigenen Formulartext, der übliche Schärfung ausdrücklich ausnimmt. Auch die Behauptung, Kameras schrieben diesen Wert nicht, ist keine ausreichende Grundlage für die Zuordnung.
|
||||
|
||||
**Empfehlung:** Allein daraus keine KI-Kennzeichnung ableiten. Als technischen Bearbeitungshinweis speichern; nur zusätzliche eindeutige KI-Aussagen berücksichtigen. Vollständige IPTC-URIs normalisieren, statt nur auf unqualifizierte Kurzbezeichnungen zu vergleichen.
|
||||
|
||||
### F04 · P1 · API-Phase und API-Vertrag passen nicht zum bestehenden System
|
||||
|
||||
**Code:** v1 nimmt ein einzelnes Bild im Feld `image` an, separat unter `POST /api/v1/press-releases/{id}/images`. Legacy verwendet `press_release_image[image]`. v1 verlangt bereits eine KI-Deklaration; der Legacy-Controller setzt dagegen fest `original`. Ein Rechtebestätigungsprotokoll gibt es in diesen Bildcontrollern noch nicht.
|
||||
|
||||
**Probleme im Entwurf:**
|
||||
|
||||
- Das JSON mit `images[]` und „base64 oder URL“ ist ein neuer Vertrag und keine bloße Ergänzung vorhandener Felder.
|
||||
- „Bild verwerfen, PM weiterverarbeiten“ passt nicht zum separaten Bildupload: Die PM existiert dabei bereits.
|
||||
- Werden diese Zugänge erst in Phase 2 berücksichtigt, können neue Uploads weiterhin ohne die neuen Nachweise entstehen. Ob ein bestimmter Partner momentan Bilder liefert, ändert nichts an den vorhandenen Endpunkten.
|
||||
|
||||
**Empfehlung:** Den bestehenden Multipart-Upload beibehalten und gezielt erweitern. Für einen ungültigen separaten Bildupload ist `422` die klare Antwort; die bestehende PM bleibt erhalten. Eine Sammelverarbeitung mit Teilfehlern nur als ausdrücklich neu spezifizierte Schnittstelle planen. Externe Bild-URLs würden zusätzlich einen abgesicherten Download mit Grenzen für Zieladressen, Weiterleitungen, Zeit und Datenmenge erfordern.
|
||||
|
||||
Zum ersten produktiven Einsatz entweder beide Uploadpfade auf den gemeinsamen Ablauf umstellen oder eine klar definierte Übergangsregel durchsetzen. Legacy-Kompatibilität bewusst entscheiden; keine heimliche Umstellung auf `original` und kein unbemerkter Bildverlust. Bestehende Response-Felder kompatibel ergänzen, inklusive effektiver Kennzeichnung und maschinenlesbarer Warncodes.
|
||||
|
||||
**Belege:** [v1-Controller](../../app/Http/Controllers/Api/V1/PressReleaseImageController.php), [v1-Request](../../app/Http/Requests/Api/V1/StorePressReleaseImageRequest.php), [Legacy-Controller](../../app/Http/Controllers/Api/Legacy/LegacyPressReleaseImageController.php), [Resource](../../app/Http/Resources/PressReleaseImageResource.php), [API-Routen](../../routes/api.php).
|
||||
|
||||
### F05 · P1 · Original und öffentliches Derivat müssen technisch strikt getrennt bleiben
|
||||
|
||||
**Code:** `ImageService::storePressReleaseImage()` erzeugt Varianten und löscht anschließend normalerweise die hochgeladene Originaldatei. `path` zeigt dann auf das Cover. Die API nennt diese Darstellung trotzdem `urls.original`. Die Medienroute liefert Bilder veröffentlichter PMs anonym aus, auch wenn deren Disk `local` heißt.
|
||||
|
||||
**Folgerung:** Das Beweisoriginal darf weder in `path` noch in der öffentlich verwendeten `variants`-Map landen. Die Bezeichnung „private Disk“ reicht als Schutz nicht aus.
|
||||
|
||||
Zusätzlich besteht ein relevanter Fehlerpfad: Wenn keine Cover-Variante entsteht, fällt der Service auf die hochgeladene Quelldatei zurück. Eine unverändert übernommene Pipeline garantiert deshalb nicht „öffentlich immer nur bereinigtes Derivat“.
|
||||
|
||||
**Empfehlung:**
|
||||
|
||||
- Original vor der bisherigen Transformation in einen gesonderten privaten Bereich kopieren, erfolgreichen Schreibvorgang prüfen und SHA-256 bilden.
|
||||
- Hash, Herkunftsprüfung und gespeichertes Original müssen dieselben Bytes betreffen.
|
||||
- Öffentliches Speichern nur bei erfolgreicher Erzeugung des vorgesehenen Derivats; kein Rückfall auf das Original.
|
||||
- Zugriff auf Beweisoriginale über eine separate, eng autorisierte Funktion. Öffentliche Medienrouten, Vorschau-Signaturen und API-Resources dürfen diesen Pfad nicht zugänglich machen.
|
||||
- Die bestehende Bedeutung von `urls.original` kompatibel behandeln und dokumentieren; sie darf nicht plötzlich auf das Beweisoriginal umgebogen werden.
|
||||
|
||||
**Belege:** [ImageService, ab Zeile 117](../../app/Services/Image/ImageService.php#L117), [Variantenfehlerpfad, ab Zeile 244](../../app/Services/Image/ImageService.php#L244), [Pfadauflösung](../../app/Models/PressReleaseImage.php), [Mediencontroller](../../app/Http/Controllers/Web/PressReleaseImageMediaController.php), [Medienberechtigungen](../../app/Http/Controllers/Web/Concerns/AuthorizesPressReleaseMediaAccess.php), [API-Resource](../../app/Http/Resources/PressReleaseImageResource.php).
|
||||
|
||||
### F06 · P1 · Das vorausgesetzte Rechtstextsystem existiert so noch nicht
|
||||
|
||||
**Code und Datenbankschema:** Vorhanden ist `legal_documents`. Es speichert Versionsmetadaten, aber keinen archivierten Textinhalt. `legal_document_sections` findet sich im Anwendungscode und abgefragten Schema nicht. Der AGB-Inhalt steht im Blade-Template.
|
||||
|
||||
**Auswirkung:** `confirmation_text_version = img-confirm-v2` alleine macht den tatsächlich angezeigten Wortlaut noch nicht dauerhaft rekonstruierbar. Abschnitt 1f ist eine zusätzliche technische Abhängigkeit und muss vor einer gültigen Bestätigung bereitstehen.
|
||||
|
||||
**Empfehlung:** Das vorhandene Versionssystem gezielt erweitern oder ein kleines, unveränderliches Archiv der Bestätigungstexte ergänzen. Speichern beziehungsweise unveränderlich referenzieren:
|
||||
|
||||
- vollständigen angezeigten Text mit Version, Sprache und aufgelösten Portalbezeichnungen;
|
||||
- die maßgebliche AGB-Fassung;
|
||||
- Datei-Hash und bestätigte Angaben einschließlich Lizenz-/Quellenangaben;
|
||||
- handelnden Benutzer und gegebenenfalls zusätzlich die Token-ID;
|
||||
- Firma/Vertretungskontext zum Bestätigungszeitpunkt.
|
||||
|
||||
Bei API-Nutzung sind Tokeninhaber und konkretes Token hilfreicher als nur `actor_type = api_key`. Bei Admin-Uploads muss erkennbar bleiben, dass der Admin gehandelt hat; das darf nicht als Bestätigung des Kunden protokolliert werden. Eine Firma oder ein Token kann später geändert beziehungsweise gelöscht werden, ohne dass damit der historische Kontext verschwinden darf.
|
||||
|
||||
Den Satz „Ohne diese Tabelle ist der Toggle im Streitfall wertlos“ würde ich ersetzen: Entscheidend ist die rekonstruierbare Bestätigung samt Identität und Inhalt, nicht ein bestimmter Tabellenname. Welche Nachweise rechtlich genügen, gehört in die vorgesehene Anwaltsprüfung.
|
||||
|
||||
**Belege:** [LegalDocument](../../app/Models/LegalDocument.php), [AGB-Template](../../resources/views/web/agb.blade.php), [bestehendes Bestätigungsmodell](../../app/Models/BookingConfirmation.php), [bestehendes KI-Protokoll](../../app/Models/PressReleaseAiDeclaration.php).
|
||||
|
||||
### F07 · P1 · Aufbewahrung und Veröffentlichung sind noch nicht durchgängig abgesichert
|
||||
|
||||
**Code:** Kunden können Bilder bei Entwürfen oder abgelehnten PMs ändern; Admins können im gemeinsamen Bildmanager auch andere Status bearbeiten, außer `Archived`. v1 beschränkt Änderungen auf Entwurf/abgelehnt. Der Legacy-Bildupload besitzt diese Statusprüfung nicht. `publish()` prüft die neuen Bildrechte naturgemäß noch nicht; die heutige KI-Vollständigkeitsprüfung erfolgt beim Einreichen.
|
||||
|
||||
**Empfehlung:** Für jedes neue Bild muss beim öffentlichen Wirksamwerden eine gültige Bestätigung zur aktuellen Datei und Deklaration vorliegen. Das betrifft auch Admin-Bildtausch nach Veröffentlichung und Legacy-Nachlieferungen. Bestehende PMs dürfen durch die neuen Regeln nicht unbeabsichtigt blockiert werden; diese Ausnahme muss ausdrücklich auf den geschützten Bestand begrenzt sein.
|
||||
|
||||
Die Löschung benötigt ebenfalls ein vollständiges Modell:
|
||||
|
||||
- `deletePressReleaseImage()` löscht heute Dateien sofort; Bildzeilen werden anschließend weich gelöscht. Der neue Beweisbestand braucht eine davon getrennte Aufbewahrungsentscheidung.
|
||||
- Die ursprüngliche Bildtabelle hat einen kaskadierenden Fremdschlüssel zur PM. Ein späteres hartes Löschen darf Bestätigungen nicht ungewollt mit entfernen. `PressReleaseMediaPrivatizer` besitzt außerdem einen gezielten `forceDelete()`-Pfad für fehlende Legacy-Dateien; dessen Bestandsscope beibehalten.
|
||||
- `legal_hold` wird erwähnt, aber nicht modelliert. Zuständigkeit, Setzen, Aufheben und Vorrang vor der Löschung fehlen.
|
||||
- Für nie veröffentlichte Bilder, entfernte Entwürfe und Bildtausch gibt es kein eindeutiges Datum „nach Depublizierung“. Dafür eigene Friststarter definieren; der bestehende Draft-Purge archiviert lediglich.
|
||||
|
||||
Den Löschmechanismus und seine Fristberechnung würde ich zusammen mit der Originalspeicherung planen und zum Livegang betriebsfähig machen. Das Wort „BALD“ ist für bereits gesammelte Beweisdaten keine belastbare Aufbewahrungsregel.
|
||||
|
||||
**Belege:** [Bildmanager, ab Zeile 292](../../resources/views/livewire/components/press-release-images-manager.blade.php#L292), [Veröffentlichung, ab Zeile 212](../../app/Services/PressRelease/PressReleaseService.php#L212), [Bildmigration](../../database/migrations/2026_04_23_171001_create_press_release_images_table.php), [Privatisierung](../../app/Services/PressRelease/PressReleaseMediaPrivatizer.php), [Draft-Archivierung](../../app/Console/Commands/PurgeExpiredPressReleaseDrafts.php).
|
||||
|
||||
### F08 · P2 · Datenmodell als Erweiterung konkretisieren
|
||||
|
||||
Der Entwurf beschreibt einige bestehende Felder als neue Spalten und lässt andere benötigte Felder offen:
|
||||
|
||||
| Punkt | Empfehlung |
|
||||
|---|---|
|
||||
| `credit` gegenüber vorhandenem `copyright` | Vorerst `copyright` als Speicherfeld für den sichtbaren Nachweis weiterverwenden; `credit` kann ein API-/Formularname sein. Eine echte Umbenennung braucht eine vollständige Migration aller Leser und Schreiber. |
|
||||
| `ai_declaration`, `license_url` | Bereits vorhanden; keine zweite Spalte anlegen. Derzeit ist `ai_declaration` ein `varchar(20)` mit PHP-Enum-Cast. |
|
||||
| `credit NOT NULL`, Originalhash, Bestätigungs-FK | Für Bestand ohne diese Daten nullable beziehungsweise anderweitig explizit migrationsfähig; Pflichtregeln für neue Uploads serverseitig durchsetzen. |
|
||||
| `rights_confirmation_id` und Rückverweis auf Bild | Zirkuläre Referenz: Bild zunächst mit leerem Bestätigungsverweis, dann Bestätigung, dann Verweis setzen; gemeinsam in einer Transaktion. Alternativ aktuelle gültige Bestätigung über eine Beziehung ermitteln. |
|
||||
| `alt_text` | Noch kein gleichnamiges Bildfeld vorhanden; heute werden `description`/`title` verwendet. Speicherung, Länge und Fallback festlegen. |
|
||||
| `credit_source` | `legacy`/`unknown` und einen Firmennamenvorschlag abbilden; „woher vorgeschlagen“ und „wer final bestätigt“ sind verschiedene Informationen. |
|
||||
| `license_url` | DB aktuell 255 Zeichen, Formularvalidierung bis 2048; Länge bei dieser Änderung konsistent festlegen. |
|
||||
| `is_ai_generated`, `license_type` | Weiterverwendung und Übergang definieren; Kunden-/Adminansicht und PDF lesen diese Felder noch. |
|
||||
|
||||
Die sichtbare Nachweiszeile sollte über eine gemeinsame Auflösung für neue Angaben und Bestandsfallbacks verfügen. Historische Details müssen nicht im vereinfachten Formular erscheinen, sollten aber erhalten bleiben.
|
||||
|
||||
**Belege:** [Bildmodell](../../app/Models/PressReleaseImage.php), [Lizenzmigration](../../database/migrations/2026_05_29_130849_add_license_fields_to_press_release_images.php), [Bildmanager](../../resources/views/livewire/components/press-release-images-manager.blade.php), [PDF-Bildangaben](../../app/Services/PressRelease/ProofPdfRenderer.php).
|
||||
|
||||
### F09 · P2 · Prüfung und Zustimmung an die konkrete Dateiversion binden
|
||||
|
||||
Eine Prüfung nach der Dateiauswahl genügt nicht, wenn zwischen Prüfung und Speichern die Datei oder die bestätigten Angaben wechseln. Im heutigen Formular gibt es noch keine Herkunftsprüfung und dementsprechend auch keine Bindung an deren Ergebnis.
|
||||
|
||||
**Empfehlung:** Prüfungsergebnis serverseitig an Upload-ID beziehungsweise Hash binden. Bei Dateiwechsel Prüfung, Vorbelegung und Zustimmung zurücksetzen. Eine manuelle Nachweisänderung nicht anschließend durch verspätete Prüfergebnisse überschreiben. Relevante Änderungen nach der Zustimmung erfordern eine neue Zustimmung.
|
||||
|
||||
Die endgültigen Werte, der Hash, der Actor und die Textversion werden serverseitig bestimmt. Öffentliche Livewire-Properties sind grundsätzlich Eingaben des Clients; eine ausgegraute Auswahl ist keine Absicherung. [Quelle: Livewire 4, Properties und Sicherheit](https://livewire.laravel.com/docs/4.x/properties).
|
||||
|
||||
Dateispeicherung und Datenbanktransaktion müssen als zusammenhängender Vorgang mit Fehlerbereinigung gestaltet werden. Ein DB-Rollback löscht keine gespeicherten Dateien. Gleichzeitige Uploads müssen außerdem die bestehende „ein Titelbild“-Regel zuverlässig einhalten. Ein passender gemeinsamer Upload-Service kann diese Regeln für Volt, v1 und Legacy bündeln; der vorhandene `ImageService` bleibt für die Bildverarbeitung zuständig.
|
||||
|
||||
### F10 · P2 · „Kein Treffer“ und „Prüfung fehlgeschlagen“ getrennt anzeigen
|
||||
|
||||
**Entwurf:** Bei Timeout wird derselbe Formularzustand wie bei fehlenden Herkunftsdaten verwendet.
|
||||
|
||||
Das ist für den Nutzer eine falsche Aussage. Eine bewusst aufwendig präparierte Datei könnte außerdem einen Timeout provozieren und so eine sonst vorgesehene Sperre umgehen.
|
||||
|
||||
**Empfehlung:** Bei Fehler „Herkunftsdaten konnten nicht geprüft werden“ anzeigen. Entscheiden, ob nur das Speichern als Entwurf erlaubt ist oder ob eine manuelle Erklärung bei ungeprüfter Herkunft ausreicht. Letzteres kann eine bewusste Produktregel sein, darf aber nicht als vollständig durchgesetzte Metadatensperre beschrieben werden. Technische Fehlerstatus, Wiederholungen und Ausfallquote erfassen.
|
||||
|
||||
### F11 · P2 · Die Kategorie „Originalfoto“ ist für Nicht-KI-Bilder zu eng
|
||||
|
||||
Das Formular erfasst auch Logos, Diagramme, Screenshots, Illustrationen und klassische Renderings. Ohne KI sind diese nicht zwingend „Originalfotos“. Ebenso beweist ein Hinweis auf eine Digitalaufnahme nicht, dass später keine KI-Bearbeitung stattgefunden hat.
|
||||
|
||||
**Empfehlung:** Die erste Option etwa „Ohne generative KI erstellt oder bearbeitet“ nennen und die Definition üblicher Bearbeitung unmittelbar ergänzen. Den internen Wert `original` kann man aus Kompatibilitätsgründen zunächst erhalten.
|
||||
|
||||
`max(original, ai_edited, ai_generated)` ist eine Portalregel, keine natürliche Rangfolge des Wahrheitsgehalts. Ein KI-erzeugter Bestandteil in einer Fotomontage macht nicht automatisch das komplette Bild zu einem vollständig KI-generierten Bild. Erst den relevanten Metadatenbefund fachlich zuordnen, danach die vereinbarte Mindestkennzeichnung anwenden. Nutzerangabe, abgeleiteter Befund und effektive Ausgabe getrennt speichern, damit die Entscheidung erklärbar bleibt.
|
||||
|
||||
### F12 · P2 · Bildnachweis, Urheber und Lizenz nicht gleichsetzen
|
||||
|
||||
Ein vorbefüllter Firmenname kann praktisch sein, ist aber nicht automatisch der richtige Bildnachweis. Dasselbe gilt für eine Agentur-Credit-Line gegenüber dem eigentlichen Urheber oder einem Copyright-Vermerk.
|
||||
|
||||
**Empfehlung:**
|
||||
|
||||
- Firmennamen nur klar als überprüfbaren Vorschlag anzeigen; vorhandene Nutzertexte nicht ersetzen.
|
||||
- Neben klassischem IPTC Credit auch dessen XMP-Entsprechung berücksichtigen. Mehrere Creator, Sprachvarianten und zu lange Werte kontrolliert normalisieren; Nachweistexte nicht still abschneiden.
|
||||
- Die Felder `creator`, `copyrightNotice` und `creditText` beim späteren Export nur mit den jeweils bekannten Informationen befüllen. Einen frei formulierten Credit nicht pauschal in alle drei Felder kopieren.
|
||||
- `license_url` und `source_url` fachlich getrennt halten, selbst wenn die Oberfläche vereinfacht wird. Ein Link zur Agentur oder Ursprungsseite ist nicht automatisch eine Lizenz.
|
||||
- Nur zulässige Linkschemata veröffentlichen und alle aus Dateien gewonnenen Texte bei HTML-/JSON-Ausgabe korrekt escapen.
|
||||
|
||||
Google unterscheidet diese Angaben ausdrücklich; `license` soll auf eine Beschreibung der tatsächlichen Nutzungsbedingungen verweisen. [Quelle: Google-Bildmetadaten](https://developers.google.com/search/docs/appearance/structured-data/image-license-metadata).
|
||||
|
||||
### F13 · P2 · Öffentliche Ausgabe und Metadatenexport genauer abgrenzen
|
||||
|
||||
Die sichtbare Zeile plus Detailinformation ist nachvollziehbar. Die heutige Artikelseite besitzt bereits Nachweiszeile und KI-Badge; die Umsetzung sollte diese konsistent weiterentwickeln.
|
||||
|
||||
**Ergänzungen zum Umfang:**
|
||||
|
||||
- Kundenansicht, Adminansicht, anonyme Vorschau und `ProofPdfRenderer` mitziehen. Historische Belegausgaben müssen zur jeweils dokumentierten Fassung passen.
|
||||
- Keine internen Notizen, IP-Hashes, privaten Pfade oder ungefilterten Tool-Dumps im Overlay beziehungsweise API-Response ausgeben. Nur ausdrücklich freigegebene Herkunftsfelder anzeigen.
|
||||
- Korrekturen mit den vorhandenen Web-Caches abstimmen. Öffentliche Mediendateien werden derzeit mit `max-age=86400` ausgeliefert; bei geänderten Derivat-Metadaten Dateiversionen beziehungsweise Cache-Verhalten berücksichtigen.
|
||||
- Das vorhandene `NewsArticle.image` ist derzeit eine URL-Liste. Eine Erweiterung um `ImageObject` ist möglich. `digitalSourceType` ist eine Schema.org-Eigenschaft; gültiges Schema und tatsächliche Unterstützung durch Suchmaschinen sind getrennt zu prüfen. [Quelle: Schema.org](https://schema.org/digitalSourceType).
|
||||
|
||||
**Technische Korrekturen zu Abschnitt 3.4:**
|
||||
|
||||
1. Der aktuelle GD-Service behält JPEG/PNG/WebP als Ausgabeformat bei; eine generelle WebP-Konvertierung existiert nicht. Der Export muss die tatsächlich erzeugten Formate unterstützen.
|
||||
2. Für WebP dokumentiert ExifTool das Schreiben von EXIF, XMP und ICC-Profilen. Klassisches `IPTC:CopyrightNotice` ist deshalb keine formatübergreifende Schreibstrategie. Passende XMP-Felder verwenden und nach dem Schreiben wieder auslesen. [Quelle: ExifTool-Tagdokumentation](https://www.exiftool.org/TagNames.pdf).
|
||||
3. Ein exportiertes XMP-Feld stellt keine C2PA-Signatur des Derivats her. Die Herkunftsanzeige muss sich ausdrücklich auf die beim Upload geprüfte Originaldatei beziehen.
|
||||
4. „Damit tragen Google, LinkedIn und andere Plattformen … weiter“ ist zu verbindlich. Metadaten schaffen eine Auswertungsmöglichkeit. Google garantiert deren Anzeige ausdrücklich nicht; für LinkedIn ist im Entwurf kein Nachweis einer solchen Garantie vorhanden. [Quelle: Google-Bildmetadaten, Troubleshooting](https://developers.google.com/search/docs/appearance/structured-data/image-license-metadata#troubleshooting).
|
||||
|
||||
### F14 · P2 · Formularzustände, Korrekturweg und Betrieb vervollständigen
|
||||
|
||||
**Formular:**
|
||||
|
||||
- E4 erlaubt eine Höherstufung, Zustand A behauptet dagegen „kann nicht geändert werden“. Bei Mindestwert `ai_edited` muss die erlaubte Wahl von `ai_generated` weiterhin möglich sein, falls diese Regel bestehen bleibt.
|
||||
- Zustand A kann laut Zuordnung auch durch unsigned IPTC entstehen. Dann ist „Diese Datei enthält Content Credentials“ falsch. Unterschiedliche Herkunftstexte vorsehen.
|
||||
- „Ausgestellt von“ nur aus den dafür geeigneten Zertifikatsinformationen ableiten; `claim_generator` ist nicht automatisch Urheber oder verifizierter Produktname. Unbekannte Aussteller und fehlende Zeitangaben brauchen Fallbacks.
|
||||
- Die Kundenoberfläche verwendet aktuell überwiegend „Sie“. Für diese Änderung dabei bleiben oder einen bewussten portalweiten Wechsel planen.
|
||||
- Ein Kontaktlink für Widersprüche reicht als Einstieg. Intern braucht es dennoch einen autorisierten, protokollierten Korrekturweg mit Grund, Bearbeiter und altem/neuem Wert. Die ursprüngliche Prüfung bleibt erhalten.
|
||||
- Für das Overlay vorhandene Flux-Komponenten prüfen. Popover und modaler Dialog benötigen unterschiedliche Fokusführung; Tastaturbedienung, Rückgabe des Fokus und zugängliche Beschriftung ausdrücklich abnehmen.
|
||||
|
||||
**Betrieb:**
|
||||
|
||||
- „Beides ist in unter einer Sekunde durch“ erst nach Messung behaupten. Ein Timeout ist keine Laufzeitgarantie.
|
||||
- Toolversionen und Binärquellen festlegen; Installation in der tatsächlichen App-/Worker-Laufzeit reproduzierbar machen. Im Sail-Projekt reicht eine Installation ausschließlich auf dem Host nicht.
|
||||
- Dateityp, Dateigröße und Bildabmessungen vor kostenintensiver Verarbeitung begrenzen. Aktuell erlaubt der Bildmanager 16 MiB, die API 8 MiB; diesen Unterschied entscheiden oder dokumentieren.
|
||||
- Prozesse mit Argumentlisten und kontrollierten lokalen Pfaden starten; CPU-/Speicher-/Ausgabegrenzen und Behandlung externer Manifestverweise festlegen.
|
||||
- Originalspeicherung, Backups, Toolpflege und Fehlerbearbeitung erzeugen Betriebsaufwand. Im Anti-Zombie-Check besser zwischen „keine zusätzlichen Kundengebühren“ und tatsächlichen Betriebskosten unterscheiden.
|
||||
|
||||
## 4. Rechtliche Annahmen, die gezielt zurück an den Anwalt sollten
|
||||
|
||||
Die vorgesehenen Anwaltsfragen sind sinnvoll. Ergänzen beziehungsweise präzisieren würde ich folgende Punkte; dies ist keine rechtliche Freigabe der Texte:
|
||||
|
||||
1. **Nachweisplatzierung und Teaser:** § 13 UrhG enthält nicht wörtlich die im Entwurf in Anführungszeichen gesetzte Vorgabe „in üblicher Weise“. Er betrifft die Anerkennung der Urheberschaft und die Bestimmung der Bezeichnung. Aus ihm folgt keine pauschale Freistellung für Vorschaubilder. „Listen/Teaser ohne Nachweis“ und unveränderte OpenGraph-Bilder deshalb anhand der tatsächlich verwendeten Lizenzen prüfen. [§ 13 UrhG](https://www.gesetze-im-internet.de/urhg/__13.html).
|
||||
2. **Aufbewahrungsfrist:** „Drei Jahre nach Depublizierung“ lässt sich nicht einfach mit der regelmäßigen Verjährung gleichsetzen. § 199 BGB knüpft deren Beginn grundsätzlich an Jahresende und Kenntnis an; § 102 UrhG verweist zudem auf weitere Vorschriften. Eine konkrete Frist samt Ausnahmen, nie veröffentlichten Uploads und Rechtsstreitfällen festlegen lassen. [§ 199 BGB](https://www.gesetze-im-internet.de/bgb/__199.html), [§ 102 UrhG](https://www.gesetze-im-internet.de/urhg/__102.html).
|
||||
3. **Entfernung von Rechteinformationen:** Prüfen, ob das Verschieben der Wiederaufnahme von Rechte-Metadaten in Phase 2 angesichts der genutzten Bilder und Lizenzen vertretbar ist. § 95c UrhG enthält ein an bestimmte Voraussetzungen gebundenes Verbot der Entfernung/Änderung von Rechteinformationen; das ist weder ein pauschales Verbot jeder Metadatenbereinigung noch automatisch irrelevant. [§ 95c UrhG](https://www.gesetze-im-internet.de/urhg/__95c.html).
|
||||
4. **Datensparsamkeit:** Die Originale enthalten möglicherweise mehr als GPS und Seriennummern, etwa Namen oder Ortsangaben. Erforderlichkeit, Zugriff, Frist, Backups und Umfang des Provenance-Snapshots konkret festlegen. Einen IP-Hash nicht als anonym garantieren; für die technische Umsetzung eher einen zweckgebundenen, geheimnisgestützten Hash mit Versionierung prüfen.
|
||||
5. **Vertragspartner und Vertretung:** Klären, wer bei Agentur-, Admin- und API-Uploads welche Erklärung für wen abgibt. Das protokollierte Firmenprofil ist nicht automatisch der Vertragspartner oder Rechteinhaber.
|
||||
6. **Zusammenspiel der Zusicherungen:** Umfang der Bearbeitungs-/Syndikationsrechte, Freistellung und Korrektur einer falschen automatischen Kennzeichnung prüfen. Das Datenbankprotokoll belegt eine abgegebene Erklärung; es beweist nicht selbst das Vorliegen aller Rechte und garantiert keinen erfolgreichen Regress.
|
||||
|
||||
## 5. Vorschlag für eine angepasste Umsetzung
|
||||
|
||||
Ich würde die geplante Architektur beibehalten, aber den ersten produktiven Lieferumfang anders schneiden:
|
||||
|
||||
| Schritt | Inhalt | Ergebnis |
|
||||
|---|---|---|
|
||||
| 1. Regeln festziehen | Bestandsschutz, Vertrauensstufen, KI-Zuordnung, API-Übergang, Korrekturweg, Aufbewahrung und Rechtstexte | Eindeutige Regeln vor Schema und Formular |
|
||||
| 2. Datenmodell ergänzen | Bestehende Werte erhalten; Herkunftsbefund, Originalreferenz, unveränderliche Bestätigung und Fristdaten ergänzen | Verlustfreie Migration |
|
||||
| 3. Gemeinsamen Upload-Ablauf bauen | Original sichern und prüfen, Nutzereingaben validieren, effektive Kennzeichnung bestimmen, Derivate und Bestätigung konsistent speichern | Dieselben Regeln für Volt, v1 und Legacy |
|
||||
| 4. Formular vereinfachen | Bestehenden Bildmanager umbauen; Vorschläge, Prüfstatus, Zustimmung und erlaubte Korrekturen | Weniger Aufwand mit nachvollziehbarer Entscheidung |
|
||||
| 5. Ausgabe und Lebenszyklus schließen | Nachweis/Overlay, Vorschau/PDF, Veröffentlichungsgates, Entfernung, Aufbewahrung, `legal_hold`, Cache-Korrekturen | Durchgängiges Verhalten bis zur Löschung |
|
||||
| 6. Weitere Verbreitung ergänzen | Formatgerechte Metadaten, strukturierte Daten und tatsächlich benötigte Feed-Verträge | Konsistente Weitergabe; juristisch erforderliche Teile gegebenenfalls bereits in Schritt 5 |
|
||||
|
||||
**Pragmatische Alternative:** Falls die C2PA-Verifikation den ersten Schritt zu groß macht, zunächst die Formularvereinfachung, nachvollziehbare Selbstdeklaration, Originalablage und versionierte Zustimmung umsetzen. Metadaten zunächst als Hinweise auslesen und speichern. Automatische Sperren erst nach einer getesteten Vertrauens- und Zuordnungslogik aktivieren. Das wäre eine bewusste Änderung von E4, die fachlich entschieden werden muss.
|
||||
|
||||
Diese Alternative vermeidet eine voreilige, teilweise falsche Automatik und liefert trotzdem früh den wesentlichen Nutzen: weniger Formularaufwand und nachvollziehbare Angaben.
|
||||
|
||||
## 6. Konkrete Abnahmetests für die spätere Implementierung
|
||||
|
||||
Für diesen reinen Dokumentationsaudit wurden keine Tests verändert oder ausgeführt. Die folgenden Fälle gehören in die Implementierung; reine Fake-Bilder reichen für die Herkunftsprüfung nicht aus.
|
||||
|
||||
| Bereich | Wesentliche Fälle |
|
||||
|---|---|
|
||||
| Migration | Vorhandene `original`-/`ai_generated`-/`ai_edited`-Werte und Credits unverändert; leere Bestandswerte bleiben darstellbar; neues Ersatzbild erhält neue Regeln |
|
||||
| Metadaten | Vertrauenswürdig signiertes KI-Bild; untrusted Signatur; beschädigte Dateibindung; unsigned XMP; widersprüchliche Quellen; mehrere Manifeste und KI-Bestandteile; fehlender oder unbekannter Source-Type |
|
||||
| Zuordnung | `algorithmicallyEnhanced` ohne generative KI erzwingt keinen KI-Hinweis; URI-Normalisierung; Screenshot/Illustration ohne KI; generativ ergänztes Foto |
|
||||
| UI-Manipulation | Gesperrte Mindestkennzeichnung kann nicht unterschritten werden; erlaubte Änderung funktioniert; Dateiwechsel und spätes Prüfergebnis verwenden keine alte Zustimmung |
|
||||
| Fehler | Timeout, fehlendes Binary, ungültiges JSON, zu großer Output, Schreibfehler und gescheiterte Derivaterzeugung; niemals versehentlich öffentliches Original |
|
||||
| Atomarität | DB-Fehler nach Dateispeicherung; Wiederholung eines Requests; parallele Titelbilduploads; keine Bestätigung für die falsche Datei |
|
||||
| Zugriff | Beweisoriginal weder öffentlich noch über normale Vorschau-/Varianten-URLs erreichbar; berechtigter interner Zugriff; keine internen Daten in API/Overlay |
|
||||
| API | v1 und Legacy; fehlende Pflichtangaben; ungültige Textversion; Statusgrenzen; Metadatenkonflikt; kompatible Responses |
|
||||
| Bestätigung | Wortlaut und Sprache später rekonstruierbar; korrekter Benutzer/Token/Admin; Bildtausch; geänderte Firma; Wechsel der Textversion bei geöffnetem Formular |
|
||||
| Lebenszyklus | Alte PM erneut eingereicht; Admin-Bildtausch an veröffentlichter PM; Entfernen; nie veröffentlichter Upload; Fristablauf; gesetzter und aufgehobener `legal_hold` |
|
||||
| Ausgabe | Beide Portale, Kunden-/Adminansicht, Vorschau, PDF; Nachweiszeile ohne Klick; kein falscher C2PA-Text; Metadaten nach Export aus allen ausgelieferten Formaten wieder lesbar |
|
||||
|
||||
Geeignete bestehende Anknüpfungspunkte:
|
||||
|
||||
- [PressReleaseImageLicenseTest](../../tests/Feature/PressReleaseImageLicenseTest.php): prüft bereits Pflichtangaben und erwartet derzeit ausdrücklich, dass die Originaldatei gelöscht wird. Diese Erwartung beim Umbau gezielt ersetzen.
|
||||
- [PressReleaseAiDeclarationTest](../../tests/Feature/PressReleaseAiDeclarationTest.php): Einreichungssperre, Deklaration und bestehender Snapshot.
|
||||
- [PressReleaseImageApiTest](../../tests/Feature/Api/V1/PressReleaseImageApiTest.php): Upload, Löschung, Größenlimit, Zugriff und KI-Pflichtangabe.
|
||||
- [PressReleaseMediaPrivatizerTest](../../tests/Feature/PressReleaseMediaPrivatizerTest.php): Umgang mit privaten und fehlenden Legacy-Medien.
|
||||
- [LegalDocumentTest](../../tests/Feature/Legal/LegalDocumentTest.php): gültige Rechtstextversion und zukünftige Fassungen.
|
||||
|
||||
## 7. Prüfgrenzen und Abschluss
|
||||
|
||||
Die Codebefunde beruhen auf den verlinkten Implementierungen und dem lesend abgefragten Schema. Externe Quellen wurden für C2PA/IPTC, Metadatenexport und die genannten gesetzlichen Bezugspunkte herangezogen. Es wurden keine echten C2PA-Dateien verarbeitet, keine Laufzeit- oder Lastmessungen durchgeführt und keine juristischen Einzelfälle bewertet. Ob einzelne API-Partner derzeit Bilder einliefern, wurde nicht anhand produktiver Uploaddaten untersucht.
|
||||
|
||||
**Fazit:** Formularvereinfachung, sichtbaren Nachweis und dokumentierte Zustimmung würde ich umsetzen. Vorher würde ich vor allem Bestandsmigration, Metadatenvertrauen, API-Einbindung und Original-/Bestätigungslebenszyklus korrigieren. Dafür ist eine gezielte Erweiterung des vorhandenen Systems ausreichend; ein kompletter Neubau ist nicht erforderlich.
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
# Kontakterreichbarkeit: DNS-/MX-Prüfung, Bounces und Veröffentlichung
|
||||
|
||||
**Stand:** 15.09.2026
|
||||
|
||||
**Status:** Entwicklungsgrundlage aus der besprochenen Empfehlung. Dieses Dokument beschreibt den vorhandenen Code und das vorgeschlagene Zielverhalten. Die Erweiterung ist noch nicht implementiert; konkrete neue Feldnamen, API-Felder und Betriebsparameter sind Umsetzungsvorschläge.
|
||||
|
||||
**Prüfumfang:** Lesender Codeabgleich im Arbeitsverzeichnis und Gegenprüfung der Mailstandards. Die produktive Konfiguration, tatsächlich laufende Jobs und konkrete Rückläufer wurden nicht geprüft.
|
||||
|
||||
## 1. Ziel und fachliche Leitlinie
|
||||
|
||||
Über die API werden Pressemitteilungen mit einer Firma und gegebenenfalls Pressekontakten angelegt. Bei Veröffentlichung oder Ablehnung werden Benachrichtigungen versendet. Ungültige Adressen verursachen Bounces und erschweren journalistische Rückfragen.
|
||||
|
||||
Die Kontaktdaten sollen bereits beim Einreichen technisch geprüft werden. Dabei gelten folgende Grundsätze:
|
||||
|
||||
- Eine DNS-/MX-Prüfung erkennt Probleme der E-Mail-Domain frühzeitig.
|
||||
- Ein positiver DNS-Befund bestätigt weder das einzelne Postfach noch die Identität oder Verantwortlichkeit des Kontakts.
|
||||
- Bounce-Verarbeitung und Versandsperren bleiben erforderlich.
|
||||
- Ein einzelner Zustellfehler führt nicht automatisch zur Ablehnung oder Depublikation einer Pressemitteilung.
|
||||
- Sind sämtliche relevanten Kontaktadressen nachweislich unzustellbar, werden neue automatische Veröffentlichungen zur Kontaktkorrektur oder manuellen Prüfung zurückgehalten.
|
||||
- Die inhaltliche Prüfung bleibt eigenständig: Gute Kontaktdaten ersetzen keine Inhaltsprüfung, schlechte Kontaktdaten sind kein Beweis für falsche Inhalte.
|
||||
|
||||
## 2. Vorhandener Stand im Code
|
||||
|
||||
| Baustein | Vorhandenes Verhalten | Bedeutung für die Erweiterung |
|
||||
|---|---|---|
|
||||
| `EmailSuppressionService` | Speichert Bounce-Ereignisse und adressbezogene Versandsperren; Hard Bounce sperrt sofort, Soft Bounces standardmäßig ab drei Ereignissen in sieben Tagen | Weiterverwenden; keine zweite unabhängige Bounce-Sperrliste aufbauen |
|
||||
| `GateOutgoingMail` | Entfernt gesperrte Empfänger unmittelbar vor dem Versand; berücksichtigt eine globale Versandpause | Auch bereits wartende Nachrichten müssen weiterhin gegen aktuelle Sperren geprüft werden |
|
||||
| `PressReleaseService::notifyAuthor()` | Bei API-Einreichungen Auswahl eines Pressekontakts, danach Firmenadresse; bei fehlenden oder gesperrten Adressen keine Nachricht | Kontaktprüfung und Empfängerauswahl aufeinander abstimmen |
|
||||
| `PressReleaseService::publish()` | Veröffentlichung und anschließende Benachrichtigung; bisher keine davon abgeleitete Kontaktfreigabe | Zentraler Ansatzpunkt für die neue Veröffentlichungsprüfung |
|
||||
| `BounceMailProcessor` | Verarbeitet Rückläufer aus dem Postfach und unterscheidet Hard/Soft | Ursachen müssen vor einer fachlichen Sperrentscheidung genauer unterschieden werden |
|
||||
| Scheduler | Bounce-Abholung alle zehn Minuten vorgesehen | Produktive Postfachkonfiguration und Ausführung bei Einführung separat verifizieren |
|
||||
|
||||
Codebelege: [Versandsperren](../../app/Services/Mail/EmailSuppressionService.php), [Versandprüfung](../../app/Listeners/GateOutgoingMail.php), [Pressemitteilungsservice](../../app/Services/PressRelease/PressReleaseService.php), [Bounce-Verarbeitung](../../app/Services/Mail/BounceMailProcessor.php), [Konfiguration](../../config/bounces.php), [Scheduler](../../routes/console.php).
|
||||
|
||||
### Bestehende Grenzen
|
||||
|
||||
1. **Policy-Fehler können Adresssperren auslösen.** Der DSN-Prozessor behandelt unter anderem `5.7.x` als Soft Bounce. Wiederholte Spamfilter-, Authentifizierungs- oder Policy-Ablehnungen können dadurch den Soft-Bounce-Schwellwert erreichen. Eine solche Sperre beweist kein ungültiges Postfach.
|
||||
2. **Die aktuelle Kontaktwahl prüft nicht alle Alternativen.** Zuerst wird der erste Pressekontakt mit E-Mail nach ID ausgewählt und anschließend dessen Sperre geprüft. Ist dieser gesperrt, folgt die Firmenadresse; ein weiterer geeigneter Pressekontakt wird dabei nicht gesucht.
|
||||
3. **Ein späterer Bounce löst keine automatische Ersatzbenachrichtigung aus.** Die Empfängerauswahl berücksichtigt die zum Auswahlzeitpunkt bekannten Sperren. Ein nachträglicher Rückläufer ist ein eigener Vorgang.
|
||||
|
||||
Diese Grenzen bei der Erweiterung berücksichtigen. Insbesondere darf die vorhandene Versandsperre nicht unverändert als Nachweis fehlender Kontakterreichbarkeit verwendet werden.
|
||||
|
||||
## 3. Was die DNS-/MX-Prüfung leisten soll
|
||||
|
||||
Für `presse@firma.example` wird die Domain `firma.example` geprüft. Ein MX-Eintrag nennt einen für die Domain vorgesehenen Mailserver. Zusätzlich muss dessen Hostname zu einer nutzbaren IP-Adresse auflösbar sein.
|
||||
|
||||
### Zu unterscheidende Ergebnisse
|
||||
|
||||
| Befund | Einordnung | Folge |
|
||||
|---|---|---|
|
||||
| Nutzbarer MX vorhanden | Mailrouting im DNS vorhanden | DNS-Prüfung bestanden; Postfach bleibt unbestätigt |
|
||||
| Kein MX, aber A-/AAAA-Eintrag der Domain | Impliziter MX als zulässiger Fallback | Als möglichen Zustellweg berücksichtigen; keine Ablehnung allein wegen fehlendem MX |
|
||||
| Domain existiert nicht (`NXDOMAIN`) | Domain zum Prüfzeitpunkt nicht vorhanden | Adresse technisch unzustellbar, bis neue Daten oder eine erneute Prüfung das Gegenteil ergeben |
|
||||
| Gültiger Null MX (`MX 0 .`) | Domain erklärt ausdrücklich, keine E-Mails anzunehmen | Keine Nachricht versenden; Kontaktkorrektur erforderlich |
|
||||
| Kein MX und kein A/AAAA bei erfolgreicher DNS-Abfrage | Kein Zustellweg im DNS vorhanden | Kontaktkorrektur erforderlich |
|
||||
| MX vorhanden, sämtliche Ziele nachweislich nicht auflösbar | Fehlerhafte Mailkonfiguration | Technisches Kontaktproblem; bei mehreren Zielen alle berücksichtigen |
|
||||
| Timeout, `SERVFAIL` oder anderer vorübergehender Resolverfehler | Ergebnis unbekannt | Erneut prüfen; nicht als ungültige Adresse oder Bounce zählen |
|
||||
| Widersprüchliche oder nicht sicher auswertbare DNS-Antwort | Ergebnis unklar | Erneute oder manuelle Prüfung; keine Vermutung als gesicherten Befund speichern |
|
||||
|
||||
Der A-/AAAA-Fallback gilt bei fehlenden MX-Einträgen. Ein vorhandener, aber defekter MX wird nicht durch beliebiges Zurückfallen auf die Webserver-IP umgangen. Null MX wird ebenfalls nicht durch einen vorhandenen A-Eintrag aufgehoben. Grundlage: [RFC 5321, Abschnitt 5.1](https://datatracker.ietf.org/doc/html/rfc5321#section-5.1), [RFC 7505](https://www.rfc-editor.org/rfc/rfc7505.html).
|
||||
|
||||
### Grenzen
|
||||
|
||||
- `presse@firma.example` und `nichtvorhanden@firma.example` können dasselbe positive DNS-Ergebnis haben.
|
||||
- Ein DNS-Eintrag belegt keine aktuelle Annahmebereitschaft des Mailservers.
|
||||
- Keine Bounces gesehen zu haben bedeutet nicht, dass das Postfach bestätigt wurde.
|
||||
- Für diese Ausbaustufe sind keine SMTP-Probeanmeldungen und keine zusätzlichen Testmails vorgesehen.
|
||||
- Eine erfolgreiche DNS-Prüfung hebt niemals eine bekannte Postfachsperre auf.
|
||||
|
||||
## 4. Drei getrennte Bewertungen
|
||||
|
||||
Die Umsetzung benötigt getrennte Informationen für unterschiedliche Entscheidungen:
|
||||
|
||||
1. **Domainprüfung:** Ist ein Zustellweg im DNS vorhanden, fehlt er nachweislich oder ist das Ergebnis offen?
|
||||
2. **Adressbezogener Versandstatus:** Darf diese konkrete E-Mail-Adresse angeschrieben werden? Ursache einer Sperre erhalten, beispielsweise ungültiges Postfach, wiederholte temporäre Fehler, Policyproblem, manuelle Sperre oder Abbestellung.
|
||||
3. **Kontaktfreigabe der Pressemitteilung:** Darf die Mitteilung den automatischen Veröffentlichungsprozess fortsetzen, wartet sie auf Prüfung oder benötigt sie eine Korrektur/manuelle Entscheidung?
|
||||
|
||||
Eine Abbestellung oder manuelle Versandsperre ist für sich genommen kein Nachweis technischer Unerreichbarkeit. Ebenso darf ein positives DNS-Ergebnis keine Abbestellung umgehen. Bestehende Regeln für Benachrichtigungen weiter beachten.
|
||||
|
||||
Die konkrete Speicherung ist bei Umsetzung an das vorhandene Schema anzupassen. Kein zusätzliches pauschales Boolean-Feld `erreichbar`, das alle drei Bewertungen vermischt.
|
||||
|
||||
## 5. Empfohlener Ablauf beim Einreichen
|
||||
|
||||
1. **Eingang speichern und Syntax validieren.** Bestehende API-Verträge und Validierung beachten. Eine externe DNS-Störung darf keine bereits eingereichten Daten verlieren lassen.
|
||||
2. **Firmenadresse und alle mit der Mitteilung verknüpften Pressekontakte erfassen.** Doppelte Adressen und Domains für die Prüfung zusammenfassen. Keine fremden Kontakte allein aufgrund derselben Domain zuordnen.
|
||||
3. **Bekannte Adressprobleme einbeziehen.** Dauerhafte Empfängerfehler von temporären Fehlern, Policyproblemen und anderen Sperrgründen unterscheiden.
|
||||
4. **DNS-Prüfung unmittelbar nach erfolgreicher Speicherung im Hintergrund starten.** Gültige Cache-Ergebnisse dürfen direkt verwendet werden. Die API muss nicht auf externe DNS-Antworten warten.
|
||||
5. **Gesamtergebnis für die Mitteilung ermitteln.** Mindestens eine technisch geeignete Adresse ohne bekannten dauerhaften Empfängerfehler reicht für die technische Kontaktanforderung. Das ist keine Bestätigung des Postfachs.
|
||||
6. **Vor automatischer Veröffentlichung zentral erneut entscheiden.** Inhaltsfreigabe und Kontaktfreigabe müssen zusammenpassen. Ein noch laufender DNS-Job darf nicht von einem schnelleren Klassifizierungsjob überholt werden.
|
||||
7. **Bei Korrektur neu prüfen und den Ablauf fortsetzen.** Prüfergebnisse alter Adressstände dürfen neue Daten nicht überschreiben. Wiederholte Jobausführung darf weder doppelte Veröffentlichung noch doppelte Benachrichtigung verursachen.
|
||||
|
||||
Eine technisch geeignete, aber beispielsweise abgemeldete Adresse darf zur technischen Bewertung beitragen; eine Benachrichtigung an sie bleibt entsprechend der geltenden Versandregeln unterdrückt.
|
||||
|
||||
## 6. Entscheidung über die Veröffentlichung
|
||||
|
||||
| Kontaktsituation | Empfohlenes Verhalten |
|
||||
|---|---|
|
||||
| Pressekontakt technisch ungeeignet, Firmenadresse geeignet | Normaler Inhalts-/Veröffentlichungsprozess; Kontaktkorrektur anzeigen |
|
||||
| Firmenadresse technisch ungeeignet, mindestens ein Pressekontakt geeignet | Normaler Prozess; Firmendatenkorrektur anzeigen |
|
||||
| Erster Pressekontakt gesperrt, weiterer Kontakt geeignet | Weitere Kontakte berücksichtigen; Versand nur an zulässige Empfänger |
|
||||
| Alle relevanten Adressen nachweislich unzustellbar | Automatische Veröffentlichung zurückhalten; Kontaktkorrektur oder manuelle Prüfung erforderlich |
|
||||
| Keine Kontaktadresse vorhanden | Kontaktanforderung nicht erfüllt; Korrektur oder dokumentierte Ausnahme erforderlich |
|
||||
| Keine geeignete Adresse bekannt und mindestens eine DNS-Prüfung ausstehend | Auf Prüfung warten; keine endgültige Ablehnung |
|
||||
| Ausschließlich Policy-/Absenderprobleme oder ein einzelner temporärer Bounce | Keine inhaltliche Ablehnung daraus ableiten; Versandproblem gesondert bearbeiten |
|
||||
| Inhaltsfilter lehnt unabhängig davon ab | Inhaltliche Ablehnung darf abgeschlossen werden; Kontaktproblem und Versandstatus separat ausweisen |
|
||||
|
||||
**Alternative Kontaktwege:** Bei manuell bestätigtem Kontakt, etwa telefonisch oder über einen verantwortlichen Ansprechpartner beim API-Einlieferer, ist eine begründete Ausnahme möglich. Eine bloß hinterlegte Telefonnummer genügt dafür nicht. Zeitpunkt, prüfende Person und Grund protokollieren; die Ausnahme hebt keine Versandsperre auf.
|
||||
|
||||
**Bereits veröffentlichte Mitteilungen:** Ein späterer Bounce erzeugt einen Prüfhinweis und gegebenenfalls eine Korrekturanforderung. Keine automatische Depublikation und keine pauschale Sperrung sämtlicher Veröffentlichungen einer Firma. Zusätzliche Auffälligkeiten können eine redaktionelle Prüfung begründen. Neue Einreichungen werden anhand des aktuellen Kontaktstands bewertet.
|
||||
|
||||
## 7. Technische Anforderungen für die Entwicklung
|
||||
|
||||
### Prüfung und Speicherung
|
||||
|
||||
- Domain normalisieren, internationale Domains korrekt in eine DNS-taugliche Darstellung überführen und die bestehende Adressnormalisierung konsistent verwenden.
|
||||
- Resolver muss `NXDOMAIN`, erfolgreiche Antworten ohne passende Records und vorübergehende Fehler unterscheiden können. Eine alleinige Ja/Nein-Abfrage auf MX reicht nicht.
|
||||
- Pro Domain Ergebnis, maschinenlesbaren Grund, Prüfzeitpunkt, Ablaufzeitpunkt und nächste Wiederholung verfügbar halten.
|
||||
- Ergebnis nach DNS-Gültigkeitsdauer zwischenspeichern; negative Antworten mit ihrer negativen Cache-Gültigkeit berücksichtigen. Resolverfehler nur kurz behandeln und erneut prüfen.
|
||||
- Bei unklarer Gültigkeitsdauer konservative, konfigurierbare Cache-Grenzen vorsehen. Ein DNS-Befund darf nicht unbegrenzt als dauerhafte Domainsperre bestehen bleiben.
|
||||
- Jobs erst nach erfolgreichem Datenbank-Commit starten. DNS-Abfragen außerhalb von Datenbanktransaktionen und Veröffentlichungssperren ausführen.
|
||||
- Wiederholungen mit begrenztem Zeitbudget und zunehmenden Abständen ausführen. Nach ausgeschöpften Versuchen zur manuellen Prüfung übergeben; keine automatische Umdeutung in „Adresse ungültig“.
|
||||
- Änderungen an Adressen oder Kontaktzuordnungen machen die bisherige Gesamtbewertung ungültig. Ergebnisse an den geprüften Datenstand binden.
|
||||
- Bei zeitversetzter Veröffentlichung abgelaufene Domainbefunde und aktuelle Adresssperren berücksichtigen. Das Veröffentlichungsdatum bleibt erhalten, solange die Kontaktprüfung wartet.
|
||||
|
||||
### Anbindung an das bestehende System
|
||||
|
||||
- API v1 und Legacy müssen dieselbe fachliche Prüfung verwenden.
|
||||
- Den vorhandenen zentralen Veröffentlichungsservice erweitern, damit Klassifizierungsjobs und zeitgesteuerte Veröffentlichung die Prüfung nicht umgehen.
|
||||
- Die Anwendung auf Kundeneinreichungen ausdrücklich festlegen; Ausgangsumfang dieses Konzepts sind API-Einreichungen. Die Herkunft muss serverseitig nachvollziehbar bleiben.
|
||||
- Einen eigenen Prüfgrund neben der Inhaltsklassifikation führen. Kontaktkorrektur nicht als inhaltlichen Blacklist-Verstoß speichern.
|
||||
- Kontaktprüfung vor endgültiger Veröffentlichung und Verbrauch eines Veröffentlichungsslots entscheiden.
|
||||
- Vorhandene adressbezogene Versandsperren zentral weiterverwenden. Domainbefunde gesondert verwalten; kein synthetischer Hard Bounce allein aus einer DNS-Abfrage.
|
||||
- Empfängerauswahl über alle zulässigen Pressekontakte und danach die Firmenadresse vornehmen; unmittelbar vor Versand erneut auf Sperren prüfen.
|
||||
- Keine pauschale Übertragung eines Postfachfehlers auf alle Adressen derselben Domain oder das gesamte Firmenkonto.
|
||||
- Historische Sperren mit unklarer Ursache zur Prüfung markieren, statt sie beim Start automatisch in Veröffentlichungssperren umzuwandeln oder freizugeben.
|
||||
|
||||
### API und Admin
|
||||
|
||||
- Einlieferern verständliche, maschinenlesbare Gründe bereitstellen, beispielsweise Domain nicht vorhanden, Domain nimmt keine Mail an, Postfach dauerhaft abgewiesen oder Prüfung ausstehend. Feldnamen und Antwortformat an die bestehenden API-Verträge anpassen.
|
||||
- Annahme der Einreichung, Kontaktprüfung, Inhaltsentscheidung und Benachrichtigungszustellung unterscheidbar anzeigen. Eine erfolgreich gespeicherte Einreichung ist noch keine Veröffentlichungszusage.
|
||||
- Im Admin „Kontaktprüfung erforderlich“ und „Prüfung ausstehend“ filterbar machen; betroffene Adressen, Gründe, Prüfzeit und Mitteilungen anzeigen.
|
||||
- Korrektur, erneute Prüfung und begründete manuelle Freigabe nachvollziehbar anbieten. Kein unbelegtes Label „Kontakt verifiziert“ nach bloßer DNS-Prüfung.
|
||||
- Kontaktprobleme dem API-Einlieferer über abrufbaren Status oder einen gebündelten Bericht zugänglich machen. Keine Korrekturanforderung an eine bereits gesperrte Adresse schicken.
|
||||
- Ein späterer Bounce bleibt auch dann sichtbar, wenn die Veröffentlichung bereits erfolgreich abgeschlossen wurde.
|
||||
|
||||
## 8. Abnahmekriterien für die Implementierung
|
||||
|
||||
Die spätere Umsetzung mit deterministischen Tests und simulierten DNS-Antworten prüfen; keine Abhängigkeit von öffentlichen DNS-Servern im Testlauf.
|
||||
|
||||
1. Gültiger MX erlaubt die Fortsetzung, bestätigt aber kein Postfach und löscht keine Bounce-Sperre.
|
||||
2. Fehlender MX mit A/AAAA wird als zulässiger Fallback behandelt.
|
||||
3. Null MX, `NXDOMAIN` sowie erfolgreich festgestelltes Fehlen sämtlicher Zustellwege führen zu nachvollziehbaren Kontaktproblemen.
|
||||
4. Ein nutzbares MX-Ziel reicht aus, wenn weitere Ziele defekt sind; vorübergehende Auflösungsfehler werden nicht als endgültiges Fehlen behandelt.
|
||||
5. Timeout und `SERVFAIL` erzeugen Wiederholungen und bei ausgeschöpftem Budget eine manuelle Prüfung, keine inhaltliche Ablehnung.
|
||||
6. Firmenadresse und alle zugeordneten Pressekontakte werden berücksichtigt; ein defekter erster Kontakt verdeckt keinen geeigneten weiteren Kontakt.
|
||||
7. Alle Adressen ungeeignet oder fehlend verhindert automatische Veröffentlichung und Slot-Verbrauch; eine dokumentierte Ausnahme ist nachvollziehbar.
|
||||
8. Policyfehler, temporäre Bounces und Abbestellungen werden nicht als dauerhafte Empfängerfehler gewertet; Versandbeschränkungen bleiben wirksam.
|
||||
9. API v1, Legacy, Klassifizierungsjob und Scheduler befolgen die gemeinsame Kontaktentscheidung ohne Umgehung oder Race Condition.
|
||||
10. Wiederholte Jobs und konkurrierende Freigaben erzeugen keine doppelte Veröffentlichung, Slot-Belastung oder Benachrichtigung.
|
||||
11. Kontaktkorrekturen verwerfen veraltete Bewertungen und ermöglichen die erneute Prüfung; Cache-Ablauf löst eine neue Domainprüfung aus.
|
||||
12. Spätere Bounces depublizieren bestehende Mitteilungen nicht automatisch und sperren keine anderen Postfächer derselben Domain.
|
||||
13. API-Status und Admin zeigen Kontaktursache, Inhaltsstatus und Versandstatus getrennt; manuelle Ausnahmen sind protokolliert.
|
||||
|
||||
## 9. Empfohlene Umsetzungsreihenfolge und offene Details
|
||||
|
||||
1. **Bounce-Ursachen präzisieren:** Dauerhafte Empfängerfehler zuverlässig von Policy-/Absenderproblemen unterscheiden; bestehende Sperren entsprechend einordnen.
|
||||
2. **DNS-Prüfung und Beobachtung ergänzen:** Ergebnisse speichern, Admin-Anzeige und API-Rückmeldung bereitstellen. Zunächst ohne automatische Veröffentlichungssperren beobachten.
|
||||
3. **Kontaktregel aktivieren:** Nach Prüfung der Ergebnisse neue API-Einreichungen entsprechend Abschnitt 6 zurückhalten; Korrektur- und Ausnahmeprozess gleichzeitig bereitstellen.
|
||||
|
||||
Vor Aktivierung konkret festlegen:
|
||||
|
||||
- Zeitbudget, Anzahl und Abstände der DNS-Wiederholungen sowie Zuständigkeit für liegengebliebene Prüfungen.
|
||||
- API-Felder und Bereitstellung eines gebündelten Berichts, falls dieser zusätzlich zum abrufbaren Status benötigt wird.
|
||||
- Berechtigung und Geltungsdauer manueller Ausnahmen.
|
||||
- Geltung für erneutes Einreichen alter Entwürfe und eine mögliche spätere Erweiterung auf Kundeneinreichungen.
|
||||
|
||||
Diese Betriebsdetails sind noch keine implementierten oder abschließend festgelegten Werte. Es ist keine nachträgliche Massen-Depublikation vorgesehen.
|
||||
|
||||
## 10. Quellen und Bezug zum bisherigen Konzept
|
||||
|
||||
- [SMTP: DNS-Auflösung und impliziter MX, RFC 5321 § 5.1](https://datatracker.ietf.org/doc/html/rfc5321#section-5.1)
|
||||
- [Null MX: Domain nimmt keine E-Mails an, RFC 7505](https://www.rfc-editor.org/rfc/rfc7505.html)
|
||||
- [Postmark: Einordnung von Bounces und Spam-Beschwerden](https://postmarkapp.com/support/article/what-are-bounces-and-spam-complaints)
|
||||
- [Bisheriges Bounce-Konzept vom 15.09.2026](../2026-09-01/bounce-mails-2026-09-15.md)
|
||||
|
||||
Dieses Dokument ergänzt das bisherige Bounce-Konzept um die Prüfung beim Einreichen und deren Auswirkungen auf die Veröffentlichung. Aussagen zum vorhandenen Verhalten beruhen auf dem oben verlinkten Code, nicht auf einer bestätigten produktiven Ausführung.
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#^Instanceof between App\\Models\\User and Illuminate\\Contracts\\Auth\\MustVerifyEmail will always evaluate to true\.$#'
|
||||
identifier: instanceof.alwaysTrue
|
||||
count: 1
|
||||
path: app/Actions/Fortify/UpdateUserProfileInformation.php
|
||||
|
||||
-
|
||||
message: '#^Expression on left side of \?\? is not nullable\.$#'
|
||||
identifier: nullCoalesce.expr
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
|
|||
|
||||
public function grantCredits(): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
||||
$this->validate([
|
||||
'grantCreditsAmount' => ['required', 'integer', 'min:1', 'max:100000'],
|
||||
'grantCreditsNote' => ['nullable', 'string', 'max:240'],
|
||||
|
|
@ -62,6 +64,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
|
|||
|
||||
public function grantFreePressReleases(): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
||||
$this->validate([
|
||||
'grantPmCount' => ['required', 'integer', 'min:1', 'max:100'],
|
||||
'grantPmNote' => ['nullable', 'string', 'max:240'],
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
|
|||
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
|
||||
use Laravel\Fortify\Features;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
|
|
@ -35,7 +36,10 @@ new #[Layout('components.layouts.app'), Title('Konto-Sicherheit')] class extends
|
|||
/**
|
||||
* Recovery-Codes werden nur direkt nach Bestätigung bzw. Neu-Erzeugung
|
||||
* angezeigt (einmalige Sichtbarkeit), nicht bei jedem Seitenaufruf.
|
||||
* Gesperrt (Audit F03): nur der Server schaltet die Anzeige frei, ein
|
||||
* Client-Update der Property darf die Codes nicht entschlüsseln.
|
||||
*/
|
||||
#[Locked]
|
||||
public bool $showRecoveryCodes = false;
|
||||
|
||||
public function mount(): void
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@ Route::middleware(['auth', 'verified', BlockAdminDuringImpersonation::class, Ens
|
|||
|
||||
Volt::route('admin/users', 'admin.users')->name('admin.users.index');
|
||||
Volt::route('admin/users/create', 'admin.users.create')->name('admin.users.create');
|
||||
Volt::route('admin/users/{id}', 'admin.users.show')->name('admin.users.show');
|
||||
// Gutschriften auf der Detailseite sind finanzwirksam: die Berechtigung
|
||||
// hängt an der Route (persistente Authorize-Middleware, greift auch für
|
||||
// Livewire-Folge-Requests) und nicht nur an mount() (Audit F08).
|
||||
Volt::route('admin/users/{id}', 'admin.users.show')->middleware('can:users:manage')->name('admin.users.show');
|
||||
Volt::route('admin/users/{id}/edit', 'admin.users.edit')->name('admin.users.edit');
|
||||
|
||||
// Interner Newsletter: Kampagnen an Bestands-User (Filter + Protokoll).
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use App\Services\Billing\AdminGrantService;
|
|||
use App\Services\Billing\CreditWalletService;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Livewire\Volt\Volt as LivewireVolt;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
|
|
@ -97,3 +98,29 @@ test('the grant panel is not available without the users manage permission', fun
|
|||
|
||||
$this->get(route('admin.users.show', $editor->id))->assertForbidden();
|
||||
});
|
||||
|
||||
test('grants are refused once the permission is revoked while the panel stays open (Audit F08)', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
|
||||
$admin = User::factory()->create(['is_active' => true]);
|
||||
$admin->assignRole('admin');
|
||||
$this->actingAs($admin);
|
||||
|
||||
// Zwei geladene Komponenten (ein 403 verwirft den Livewire-Snapshot).
|
||||
$credits = LivewireVolt::test('admin.users.show', ['id' => $admin->id]);
|
||||
$releases = LivewireVolt::test('admin.users.show', ['id' => $admin->id]);
|
||||
|
||||
// Herabstufung, während die Komponenten bereits geladen sind.
|
||||
$admin->syncRoles(['editor']);
|
||||
app()[PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
$credits->set('grantCreditsAmount', 250)->call('grantCredits')->assertForbidden();
|
||||
$releases->set('grantPmCount', 2)->call('grantFreePressReleases')->assertForbidden();
|
||||
|
||||
expect(app(CreditWalletService::class)->balance($admin))->toBe(0)
|
||||
->and($admin->singlePurchases()->count())->toBe(0);
|
||||
|
||||
// Die Route selbst verlangt die Berechtigung – nicht nur mount().
|
||||
$this->get(route('admin.users.show', $admin->id))->assertForbidden();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -290,3 +290,45 @@ test('the honeypot blocks bots without creating an account', function () {
|
|||
Mail::assertNothingOutgoing();
|
||||
expect(User::query()->where('email', 'bot-target@example.test')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('an unverified email earns no company membership on password login (Audit F01)', function () {
|
||||
/** @var TestCase $this */
|
||||
// Ein Konto hat seine Adresse auf eine fremde Firmen-Adresse gewechselt
|
||||
// und ist deshalb unverifiziert – der Login-Sync darf daraus keine
|
||||
// Firmenrechte ableiten, solange das Postfach nicht nachgewiesen ist.
|
||||
$user = User::factory()->unverified()->create(['email' => 'fremd@example.test', 'is_active' => true]);
|
||||
$user->assignRole('customer');
|
||||
|
||||
$company = Company::factory()->presseecho()->create(['email' => 'fremd@example.test']);
|
||||
Contact::factory()->for($company)->create(['email' => 'fremd@example.test', 'portal' => $company->portal->value]);
|
||||
|
||||
Volt::test('auth.login')
|
||||
->set('email', 'fremd@example.test')
|
||||
->set('password', 'password')
|
||||
->call('login')
|
||||
->assertRedirect(route('verification.notice', absolute: false));
|
||||
|
||||
expect($user->fresh()->accessibleCompanyIds())->not->toContain($company->id);
|
||||
expect($user->contacts()->count())->toBe(0);
|
||||
|
||||
// Nach Verifikation greift der Sync beim nächsten Login wie bisher.
|
||||
$user->forceFill(['email_verified_at' => now()])->save();
|
||||
app(ContactAccessService::class)->syncMembershipsForEmail($user->fresh());
|
||||
|
||||
expect($user->fresh()->accessibleCompanyIds())->toContain($company->id);
|
||||
});
|
||||
|
||||
test('the membership sync ignores unverified and deactivated users regardless of the caller (Audit F01)', function () {
|
||||
/** @var TestCase $this */
|
||||
$company = Company::factory()->presseecho()->create(['email' => 'sync-guard@example.test']);
|
||||
|
||||
$unverified = User::factory()->unverified()->create(['email' => 'sync-guard@example.test', 'is_active' => true]);
|
||||
app(ContactAccessService::class)->syncMembershipsForEmail($unverified);
|
||||
expect($unverified->fresh()->accessibleCompanyIds())->not->toContain($company->id);
|
||||
|
||||
$unverified->forceDelete();
|
||||
|
||||
$inactive = User::factory()->create(['email' => 'sync-guard@example.test', 'is_active' => false]);
|
||||
app(ContactAccessService::class)->syncMembershipsForEmail($inactive);
|
||||
expect($inactive->fresh()->accessibleCompanyIds())->not->toContain($company->id);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use Database\Seeders\RolesAndPermissionsSeeder;
|
|||
use Laravel\Socialite\Contracts\Provider;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Laravel\Socialite\Two\User as SocialiteUser;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\TestCase;
|
||||
|
||||
beforeEach(function () {
|
||||
|
|
@ -103,3 +104,22 @@ test('a failed google callback redirects back to login with an error', function
|
|||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('google login hands an account with confirmed two-factor to the challenge (Audit F02)', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create(['email' => 'totp-google@example.test', 'is_active' => true]);
|
||||
$user->assignRole('customer');
|
||||
$user->forceFill([
|
||||
'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
])->save();
|
||||
|
||||
fakeGoogleUser('g-totp-1', 'totp-google@example.test', 'Totp');
|
||||
|
||||
$this->get(route('oauth.google.callback'))
|
||||
->assertRedirect(route('two-factor.challenge'));
|
||||
|
||||
$this->assertGuest();
|
||||
expect(session('login.id'))->toBe($user->id);
|
||||
expect($user->fresh()->last_login_at)->toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Tests\TestCase;
|
||||
|
||||
test('active admin and editor users can access admin area', function () {
|
||||
|
|
@ -64,3 +65,18 @@ test('api-only and inactive users cannot access customer area', function () {
|
|||
expect($apiOnly->canAccessCustomer())->toBeFalse();
|
||||
expect($inactiveCustomer->canAccessCustomer())->toBeFalse();
|
||||
});
|
||||
|
||||
test('a deactivated super admin is locked out of both panels despite the gate shortcut (Audit F09)', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create([
|
||||
'is_active' => false,
|
||||
'is_super_admin' => true,
|
||||
]);
|
||||
|
||||
expect($user->canAccessAdmin())->toBeFalse();
|
||||
expect(Gate::forUser($user)->allows('access-admin'))->toBeFalse();
|
||||
expect(Gate::forUser($user)->allows('access-customer'))->toBeFalse();
|
||||
|
||||
$this->actingAs($user)->get(route('dashboard'))->assertForbidden();
|
||||
$this->actingAs($user)->get(route('me.dashboard'))->assertForbidden();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use Database\Seeders\RolesAndPermissionsSeeder;
|
|||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
|
||||
use Livewire\Volt\Volt as LivewireVolt;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\TestCase;
|
||||
|
|
@ -523,3 +524,35 @@ test('legacy invoice policy denies access to invoices of other users', function
|
|||
expect($intruder->can('view', $invoice))->toBeFalse();
|
||||
expect($intruder->can('downloadPdf', $invoice))->toBeFalse();
|
||||
});
|
||||
|
||||
test('the fortify profile-information endpoint is switched off (Audit F01)', function () {
|
||||
/** @var TestCase $this */
|
||||
$customer = User::factory()->create([
|
||||
'is_active' => true,
|
||||
'email' => 'eigene@example.com',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($customer)
|
||||
->put('/user/profile-information', ['name' => $customer->name, 'email' => 'fremd@example.com'])
|
||||
->assertNotFound();
|
||||
|
||||
expect($customer->fresh()->email)->toBe('eigene@example.com');
|
||||
expect($customer->fresh()->email_verified_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
test('recovery codes cannot be revealed by mutating the component property (Audit F03)', function () {
|
||||
/** @var TestCase $this */
|
||||
$customer = User::factory()->create(['is_active' => true]);
|
||||
$customer->forceFill([
|
||||
'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()),
|
||||
'two_factor_recovery_codes' => encrypt(json_encode(['geheim-code-eins', 'geheim-code-zwei'])),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$this->actingAs($customer);
|
||||
|
||||
LivewireVolt::test('customer.security')
|
||||
->set('showRecoveryCodes', true)
|
||||
->assertDontSee('geheim-code-eins');
|
||||
})->throws(CannotUpdateLockedPropertyException::class);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue