images: Titelbild-Formular v2, Admin-Upload mit Kundenbestätigung, Entfernen mit Sofortlöschung (Bildkennzeichnung Block B)
- press-release-images-manager-v2 hinter IMAGE_RIGHTS_V2: drei Blöcke, Vorbelegung aus Metadaten, Bestätigung mit archiviertem Wortlaut, Bild-Bindung über Hash, Fokuspunkt und Letterbox, Zustände absent/prefilled/error - CustomerImageConfirmation: Magic-Link an den Kontoinhaber, Karte auf der Kundenseite, Kanal magic_link - PressReleaseImageRemover: nie veröffentlicht = alles sofort weg, veröffentlicht = Soft Delete, Original bleibt - ConversionSource::MailImageConfirmation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
214aea8910
commit
3d3ddc2dc8
11 changed files with 1380 additions and 5 deletions
|
|
@ -23,6 +23,9 @@ enum ConversionSource: string
|
|||
|
||||
case ProfileClaim = 'profile_claim';
|
||||
|
||||
/** „Bild bestätigen" nach einem Admin-Upload (Bildkennzeichnung B.4, E10). */
|
||||
case MailImageConfirmation = 'mail_image_confirmation';
|
||||
|
||||
case Organic = 'organic';
|
||||
|
||||
public function label(): string
|
||||
|
|
@ -32,6 +35,7 @@ enum ConversionSource: string
|
|||
self::MailReject => 'Ablehnungsmail',
|
||||
self::MailReview => 'Prüfhinweis-Mail',
|
||||
self::ProfileClaim => 'Hinweis auf der Newsroom-Seite',
|
||||
self::MailImageConfirmation => 'Mail „Bild bestätigen"',
|
||||
self::Organic => 'ohne zugeordnete Herkunft',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
44
app/Mail/PressReleaseImageConfirmationRequested.php
Normal file
44
app/Mail/PressReleaseImageConfirmationRequested.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* „Bild bestätigen" (Decision-Update E10): Ein Admin hat im Auftrag des
|
||||
* Kunden ein Titelbild hochgeladen. Veröffentlicht wird es erst, wenn der
|
||||
* Kontoinhaber die Rechtebestätigung selbst abgibt — der Magic-Link führt
|
||||
* direkt auf die Meldung im Konto.
|
||||
*/
|
||||
class PressReleaseImageConfirmationRequested extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public readonly PressRelease $pressRelease,
|
||||
public readonly PressReleaseImage $image,
|
||||
public readonly string $confirmationUrl,
|
||||
public readonly Carbon $expiresAt,
|
||||
public readonly ?string $recipientName,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Bitte bestätigen: Titelbild für Ihre Pressemitteilung',
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(view: 'emails.press-release.image-confirmation');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\PressRelease\Images;
|
||||
|
||||
use App\Enums\ConfirmationChannel;
|
||||
use App\Enums\ConversionSource;
|
||||
use App\Mail\PressReleaseImageConfirmationRequested;
|
||||
use App\Models\ImageRightsConfirmation;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
/**
|
||||
* Kundenbestätigung für Admin-Uploads (Decision-Update E10, Block B.4):
|
||||
* Der Kontoinhaber der Meldung bekommt einen Magic-Link „Bild bestätigen",
|
||||
* der ihn auf die Meldung im Konto führt. Dort gibt er dieselbe Bestätigung
|
||||
* ab wie beim eigenen Upload; das Protokoll trägt den Kanal `magic_link`.
|
||||
*/
|
||||
class CustomerImageConfirmation
|
||||
{
|
||||
public const LINK_TTL_MINUTES = 60 * 24 * 3;
|
||||
|
||||
public function __construct(
|
||||
private readonly MagicLinkGenerator $magicLinks,
|
||||
private readonly PressReleaseImageUploader $uploader,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Link erzeugen und Mail an den Kontoinhaber schicken. Erneut aufrufbar;
|
||||
* ein neuer Link entwertet den alten (MagicLinkGenerator).
|
||||
*/
|
||||
public function request(PressReleaseImage $image, ?string $requestedIp = null): bool
|
||||
{
|
||||
$pressRelease = $image->pressRelease;
|
||||
$owner = $pressRelease?->user;
|
||||
|
||||
if ($pressRelease === null || $owner === null || blank($owner->email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$link = $this->magicLinks->createLoginLink(
|
||||
$owner,
|
||||
$requestedIp,
|
||||
self::LINK_TTL_MINUTES,
|
||||
ConversionSource::MailImageConfirmation,
|
||||
[
|
||||
'company_id' => $pressRelease->company_id,
|
||||
'press_release_id' => $pressRelease->id,
|
||||
'image_confirmation' => $image->id,
|
||||
],
|
||||
);
|
||||
|
||||
if ($image->customer_confirmation_requested_at === null) {
|
||||
$image->forceFill(['customer_confirmation_requested_at' => now()])->save();
|
||||
}
|
||||
|
||||
Mail::to($owner->email)->queue(
|
||||
(new PressReleaseImageConfirmationRequested(
|
||||
$pressRelease,
|
||||
$image,
|
||||
route('magic-links.consume', ['token' => $link['plain_token']]),
|
||||
$link['expires_at'],
|
||||
$owner->name,
|
||||
))->afterCommit(),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestätigung des Kontoinhabers im Konto (Kanal magic_link).
|
||||
*/
|
||||
public function confirm(PressReleaseImage $image, User $customer, ?string $legalTextKey, ?string $ip, ?string $userAgent, ?string $agbUrl = null): ImageRightsConfirmation
|
||||
{
|
||||
return $this->uploader->confirmExisting(
|
||||
$image,
|
||||
new ImageUploadActor($customer, ConfirmationChannel::MagicLink, null, $ip, $userAgent),
|
||||
$legalTextKey,
|
||||
$agbUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\PressRelease\Images;
|
||||
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Services\Image\ImageService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Entfernt ein Titelbild (Bildkennzeichnung E.3, Decision-Update E4):
|
||||
*
|
||||
* - nie veröffentlicht: Derivate, Original, Snapshot und Bestätigung sofort
|
||||
* und vollständig weg, die Zeile wird hart gelöscht;
|
||||
* - veröffentlicht: Derivate weg, Zeile weich gelöscht; Original, Snapshot
|
||||
* und Bestätigung bleiben für das Beweisarchiv (Block F übernimmt die
|
||||
* Überführung und die Frist).
|
||||
*
|
||||
* Bestand ohne `rules_version` hat weder Original noch Protokoll und wird
|
||||
* wie bisher behandelt (Derivate weg, Soft Delete).
|
||||
*/
|
||||
class PressReleaseImageRemover
|
||||
{
|
||||
public function __construct(private readonly ImageService $imageService) {}
|
||||
|
||||
public function remove(PressReleaseImage $image): void
|
||||
{
|
||||
$this->imageService->deletePressReleaseImage($image->disk, $image->path, $image->variants);
|
||||
|
||||
if ($image->wasEverPublished() || $image->isLegacyRules()) {
|
||||
$image->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($image): void {
|
||||
foreach ($image->rightsConfirmations()->get() as $confirmation) {
|
||||
$confirmation->purgeForUnpublished();
|
||||
}
|
||||
|
||||
if ($image->original_path !== null) {
|
||||
Storage::disk($image->original_disk ?? (string) config('images.originals_disk'))->delete($image->original_path);
|
||||
}
|
||||
|
||||
if ($image->published_snapshot_path !== null) {
|
||||
Storage::disk($image->original_disk ?? (string) config('images.originals_disk'))->delete($image->published_snapshot_path);
|
||||
}
|
||||
|
||||
$image->forceDelete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +182,34 @@ class PressReleaseImageUploader
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nachträgliche Bestätigung eines bestehenden Bildes durch den
|
||||
* Kontoinhaber (Admin-Upload, Decision-Update E10) — dieselbe Prüfung
|
||||
* der Textkennung, dasselbe Protokoll, gebunden an den aktuellen Hash.
|
||||
*/
|
||||
public function confirmExisting(PressReleaseImage $image, ImageUploadActor $actor, ?string $legalTextKey, ?string $agbUrl = null): ImageRightsConfirmation
|
||||
{
|
||||
if (! $actor->confirmsImmediately()) {
|
||||
throw new ImageUploadException(__('Die Bestätigung muss vom Kontoinhaber abgegeben werden.'), 'rights_confirmed');
|
||||
}
|
||||
|
||||
$legalText = $this->resolveLegalText(
|
||||
new ImageSubmission(copyright: (string) $image->copyright, aiDeclaration: $image->ai_declaration, rightsConfirmed: true, legalTextKey: $legalTextKey),
|
||||
$actor,
|
||||
);
|
||||
|
||||
if ($legalText === null || $image->original_sha256 === null) {
|
||||
throw new ImageUploadException(__('Dieses Bild kann nicht nachträglich bestätigt werden.'), 'rights_confirmed');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($image, $actor, $legalText, $agbUrl): ImageRightsConfirmation {
|
||||
$confirmation = $this->recordConfirmation($image, $image->pressRelease, $actor, $legalText, $agbUrl);
|
||||
$image->forceFill(['rights_confirmed_at' => now()])->save();
|
||||
|
||||
return $confirmation;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Portalregel (A.4, F11): Ein vertrauenswürdig geprüfter Befund setzt
|
||||
* die Mindestkennzeichnung, die Kundenangabe darf nur verschärfen.
|
||||
|
|
@ -255,11 +283,11 @@ class PressReleaseImageUploader
|
|||
return $current;
|
||||
}
|
||||
|
||||
private function recordConfirmation(PressReleaseImage $image, PressRelease $pressRelease, ImageUploadActor $actor, LegalText $legalText, ?string $agbUrl): void
|
||||
private function recordConfirmation(PressReleaseImage $image, PressRelease $pressRelease, ImageUploadActor $actor, LegalText $legalText, ?string $agbUrl): ImageRightsConfirmation
|
||||
{
|
||||
$variables = $this->textRenderer->variables($agbUrl);
|
||||
|
||||
ImageRightsConfirmation::query()->create([
|
||||
return ImageRightsConfirmation::query()->create([
|
||||
'subject_type' => $image->getMorphClass(),
|
||||
'subject_id' => $image->getKey(),
|
||||
'press_release_id' => $pressRelease->getKey(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue