Neuer ActivityType "Mail-Abmeldung": Der Unsubscribe-Controller protokolliert Abbestellen (Link-Klick und RFC-8058-One-Click) sowie das Wieder-Aktivieren — nur bei echter Zustandsänderung, wiederholte Klicks erzeugen keine Doppel-Einträge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
116 lines
3.9 KiB
PHP
116 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Enums\ActivityType;
|
|
use App\Enums\EmailNotificationType;
|
|
use App\Models\User;
|
|
use App\Services\ActivityLog;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Illuminate\View\View;
|
|
|
|
/**
|
|
* One-Click-Abmeldung von Benachrichtigungs-Mails, ohne Login.
|
|
*
|
|
* Die Links in den Mail-Footern sind signiert und ohne Ablauf (Links aus
|
|
* alten Mails bleiben gültig). Der GET-Aufruf trägt den Empfänger direkt
|
|
* aus und zeigt die Bestätigungsseite mit der Möglichkeit, die Abmeldung
|
|
* rückgängig zu machen. Der POST auf dieselbe URL bedient den
|
|
* List-Unsubscribe-Header (RFC 8058) — Mail-Clients senden ihn ohne
|
|
* Nutzer-Interaktion auf der Seite.
|
|
*
|
|
* Jede tatsächliche Zustandsänderung landet im Aktivitäts-Verlauf
|
|
* (Admin ▸ Reports ▸ Verlauf) — wiederholte Klicks erzeugen keine
|
|
* Doppel-Einträge.
|
|
*/
|
|
class EmailUnsubscribeController extends Controller
|
|
{
|
|
public function __construct(private readonly ActivityLog $activityLog) {}
|
|
|
|
public function show(Request $request, User $user, string $type): View
|
|
{
|
|
$notificationType = $this->resolveType($type);
|
|
|
|
if (! $request->hasValidSignature()) {
|
|
return view('web.email-unsubscribe', [
|
|
'state' => 'invalid',
|
|
'notificationType' => $notificationType,
|
|
]);
|
|
}
|
|
|
|
$this->optOut($user, $notificationType, 'link');
|
|
|
|
return view('web.email-unsubscribe', [
|
|
'state' => 'unsubscribed',
|
|
'notificationType' => $notificationType,
|
|
'resubscribeUrl' => URL::signedRoute('email-preferences.resubscribe', [
|
|
'user' => $user->id,
|
|
'type' => $notificationType->value,
|
|
]),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* RFC-8058-Endpunkt: Mail-Clients senden `List-Unsubscribe=One-Click`
|
|
* per POST, ohne die Seite zu öffnen. Antwort bewusst schlicht (2xx).
|
|
*/
|
|
public function oneClickUnsubscribe(Request $request, User $user, string $type): Response
|
|
{
|
|
abort_unless($request->hasValidSignature(), 403);
|
|
|
|
$this->optOut($user, $this->resolveType($type), 'one-click');
|
|
|
|
return response('Sie wurden erfolgreich abgemeldet.');
|
|
}
|
|
|
|
public function resubscribe(Request $request, User $user, string $type): View|RedirectResponse
|
|
{
|
|
abort_unless($request->hasValidSignature(), 403);
|
|
|
|
$notificationType = $this->resolveType($type);
|
|
|
|
if (! $user->wantsEmailNotification($notificationType)) {
|
|
$user->optIntoEmailNotification($notificationType);
|
|
|
|
$this->activityLog->record(
|
|
ActivityType::EmailUnsubscribe,
|
|
sprintf('%s (%s) hat %s wieder aktiviert.', $user->name, $user->email, $notificationType->unsubscribeLabel()),
|
|
user: $user,
|
|
meta: ['type' => $notificationType->value, 'action' => 'resubscribe'],
|
|
);
|
|
}
|
|
|
|
return view('web.email-unsubscribe', [
|
|
'state' => 'resubscribed',
|
|
'notificationType' => $notificationType,
|
|
]);
|
|
}
|
|
|
|
private function optOut(User $user, EmailNotificationType $notificationType, string $method): void
|
|
{
|
|
if (! $user->wantsEmailNotification($notificationType)) {
|
|
return;
|
|
}
|
|
|
|
$user->optOutOfEmailNotification($notificationType);
|
|
|
|
$this->activityLog->record(
|
|
ActivityType::EmailUnsubscribe,
|
|
sprintf('%s (%s) hat %s abbestellt.', $user->name, $user->email, $notificationType->unsubscribeLabel()),
|
|
user: $user,
|
|
meta: ['type' => $notificationType->value, 'action' => 'unsubscribe', 'method' => $method],
|
|
);
|
|
}
|
|
|
|
private function resolveType(string $type): EmailNotificationType
|
|
{
|
|
$notificationType = EmailNotificationType::tryFrom($type);
|
|
|
|
abort_if($notificationType === null, 404);
|
|
|
|
return $notificationType;
|
|
}
|
|
}
|