web: Mail-Abmeldungen landen im Aktivitäts-Verlauf

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>
This commit is contained in:
Kevin Adametz 2026-07-21 10:26:49 +00:00
parent 215cbcbdbf
commit ab0030d71e
4 changed files with 55 additions and 4 deletions

View file

@ -2,8 +2,10 @@
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;
@ -19,9 +21,15 @@ use Illuminate\View\View;
* 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);
@ -33,7 +41,7 @@ class EmailUnsubscribeController extends Controller
]);
}
$user->optOutOfEmailNotification($notificationType);
$this->optOut($user, $notificationType, 'link');
return view('web.email-unsubscribe', [
'state' => 'unsubscribed',
@ -53,7 +61,7 @@ class EmailUnsubscribeController extends Controller
{
abort_unless($request->hasValidSignature(), 403);
$user->optOutOfEmailNotification($this->resolveType($type));
$this->optOut($user, $this->resolveType($type), 'one-click');
return response('Sie wurden erfolgreich abgemeldet.');
}
@ -64,7 +72,16 @@ class EmailUnsubscribeController extends Controller
$notificationType = $this->resolveType($type);
$user->optIntoEmailNotification($notificationType);
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',
@ -72,6 +89,22 @@ class EmailUnsubscribeController extends Controller
]);
}
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);