auth/mail: Magic-Link-2FA (M1) + Mailversand über die Queue

- Magic-Link-Login übergibt bei aktiviertem 2FA an die 2FA-Challenge,
  statt sie zu umgehen (M1).
- Bestätigungs-/Benachrichtigungs-Mails (Kontakt, Recht, Kündigung,
  Widerruf, Magic-Link) laufen als ShouldQueue+afterCommit über die Queue,
  damit die Response nicht am SMTP-Roundtrip hängt.

Hinweis: Der Queue-Worker muss im Betrieb laufen und überwacht werden, da
fristrelevante Bestätigungen darüber zugestellt werden (siehe
Betriebsüberwachung).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-14 13:49:21 +00:00
parent 6764fb9bc2
commit ea8d017445
20 changed files with 135 additions and 60 deletions

View file

@ -45,18 +45,35 @@ class MagicLinkConsumeController extends Controller
return redirect()->route('login')->with('status', __('The magic login link has expired or was already used.'));
}
$magicLink->user->update([
$user = $magicLink->user;
// 2FA aktiv: nicht direkt einloggen, sondern – wie der Passwort-Login –
// in Fortifys Challenge übergeben. Der Magic-Link (Besitz des Postfachs)
// ersetzt nur das Passwort, nicht den zweiten Faktor. Der Link ist bereits
// atomar verbraucht; bricht der Nutzer die Challenge ab, fordert er einen
// neuen an.
if ($user->hasEnabledTwoFactorAuthentication()) {
$request->session()->put([
'login.id' => $user->getKey(),
'login.remember' => false,
]);
$request->session()->regenerate();
return redirect()->route('two-factor.challenge');
}
$user->update([
'last_login_at' => now(),
'last_login_ip' => $request->ip(),
]);
Auth::guard('web')->login($magicLink->user);
Auth::guard('web')->login($user);
$request->session()->regenerate();
// Rollensicherer Redirect ohne intended(): eine als Gast besuchte
// Admin-URL (z. B. /dashboard) darf einen Customer nach dem
// Magic-Link-Login nicht in den 403-Admin-Bereich schicken.
$user = $magicLink->user;
$home = $user->canAccessAdmin()
? route('dashboard', absolute: false)

View file

@ -95,9 +95,13 @@ class LegalRequestController extends Controller
// Vorgangsnummer (sofern eine E-Mail-Adresse angegeben wurde) +
// interner Hinweis ans Melde-Postfach.
if ($legalRequest->requester_email) {
Mail::to($legalRequest->requester_email)->send(new LegalRequestReceived($legalRequest));
Mail::to($legalRequest->requester_email)->queue(
(new LegalRequestReceived($legalRequest))->afterCommit()
);
}
Mail::to($this->operatorRecipient())->send(new LegalRequestSubmitted($legalRequest));
Mail::to($this->operatorRecipient())->queue(
(new LegalRequestSubmitted($legalRequest))->afterCommit()
);
return redirect()
->route('release.detail', ['slug' => $release->slug])

View file

@ -77,10 +77,17 @@ class ReportFormController extends Controller
'requester_ip' => $request->ip(),
]);
// Unverzügliche Eingangsbestätigung (DSA Art. 16 Abs. 4) + interner
// Hinweis ans Melde-Postfach, damit Fristen nicht an der Queue hängen.
Mail::to($legalRequest->requester_email)->send(new LegalRequestReceived($legalRequest));
Mail::to($this->operatorRecipient($request))->send(new LegalRequestSubmitted($legalRequest));
// Eingangsbestätigung (DSA Art. 16 Abs. 4) + interner Hinweis ans
// Melde-Postfach. Versand über die Queue (afterCommit), damit die
// Response nicht am SMTP-Roundtrip hängt — setzt einen laufenden
// Queue-Worker voraus (Ops-Monitoring erforderlich, da die Zustellung
// fristrelevant ist).
Mail::to($legalRequest->requester_email)->queue(
(new LegalRequestReceived($legalRequest))->afterCommit()
);
Mail::to($this->operatorRecipient($request))->queue(
(new LegalRequestSubmitted($legalRequest))->afterCommit()
);
return back()->with('report-status', __('static.report_form.flash_received', [
'reference' => $legalRequest->reference(),

View file

@ -71,8 +71,12 @@ class TerminationController extends Controller
// § 312k Abs. 4 BGB: Zugang der Kündigungserklärung sofort auf
// elektronischem Wege bestätigen (Inhalt, Datum, Uhrzeit).
Mail::to($terminationRequest->email)->send(new TerminationRequestReceived($terminationRequest));
Mail::to($this->operatorRecipient())->send(new TerminationRequestSubmitted($terminationRequest));
Mail::to($terminationRequest->email)->queue(
(new TerminationRequestReceived($terminationRequest))->afterCommit()
);
Mail::to($this->operatorRecipient())->queue(
(new TerminationRequestSubmitted($terminationRequest))->afterCommit()
);
return $this->done();
}

View file

@ -49,8 +49,12 @@ class WithdrawalController extends Controller
]);
// Gesetzlich vorgeschrieben: sofortige automatische Eingangsbestätigung.
Mail::to($withdrawalRequest->email)->send(new WithdrawalRequestReceived($withdrawalRequest));
Mail::to($this->operatorRecipient())->send(new WithdrawalRequestSubmitted($withdrawalRequest));
Mail::to($withdrawalRequest->email)->queue(
(new WithdrawalRequestReceived($withdrawalRequest))->afterCommit()
);
Mail::to($this->operatorRecipient())->queue(
(new WithdrawalRequestSubmitted($withdrawalRequest))->afterCommit()
);
return $this->done();
}