presseportale/resources/views/livewire/auth/magic-link.blade.php
Kevin Adametz b1b832df4c 5.5: <x-session-status> + 4 Auth-Seiten umgestellt
Self-conditional Component fuer den session('status')-Block (field-status +
role=status); Margin per class. Ersetzt den identischen 5-Zeilen-Block in
login, forgot-password, register, magic-link.

Bewusst NICHT umgestellt: verify-email (fixe Meldung statt session('status'),
Bedingung ===verification-link-sent, mb-6) und settings/profile (eigener
Kontext) — bleiben unveraendert. Alle Views kompilieren, 95 Auth-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:41:46 +00:00

129 lines
4.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Services\Auth\ContactAccessService;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Anmeldung per E-Mail-Link', 'eyebrow' => 'Ohne Passwort anmelden', 'topRightLabel' => 'Lieber mit Passwort?', 'topRightLinkText' => 'Anmelden', 'topRightLinkHref' => '/login'])] class extends Component {
#[Validate('required|string|email')]
public string $email = '';
// Honeypot gegen einfache Bots – muss leer bleiben.
public string $website = '';
/**
* Versendet einen Anmeldelink. Deckt Bestandskonten UND hinterlegte
* Pressekontakte (lazy Account) über denselben Service-Eintritt ab.
* Enumeration-sicher: identische neutrale Antwort in jedem Fall.
*/
public function requestLink(ContactAccessService $contactAccess): void
{
$this->validate();
if ($this->website !== '') {
// Bot: identische neutrale Antwort, keine Aktion.
$this->sent();
return;
}
$this->ensureIsNotRateLimited();
RateLimiter::hit($this->throttleKey(), 3600);
RateLimiter::hit($this->ipThrottleKey(), 3600);
$contactAccess->requestMagicAccess($this->email, request()->ip());
$this->sent();
}
private function sent(): void
{
$this->reset('email');
session()->flash('status', __('Falls für diese E-Mail-Adresse ein aktives Konto oder ein hinterlegter Pressekontakt existiert, haben wir Ihnen einen Anmeldelink geschickt.'));
}
/**
* Versand drosseln: pro E-Mail+IP (gegen Mail-Fluten eines Accounts und das
* laufende Entwerten alter Links) und zusätzlich pro IP (gegen das
* Durchprobieren vieler Accounts von einer Quelle).
*/
protected function ensureIsNotRateLimited(): void
{
$withinEmail = ! RateLimiter::tooManyAttempts($this->throttleKey(), 3);
$withinIp = ! RateLimiter::tooManyAttempts($this->ipThrottleKey(), 15);
if ($withinEmail && $withinIp) {
return;
}
$seconds = max(
RateLimiter::availableIn($this->throttleKey()),
RateLimiter::availableIn($this->ipThrottleKey()),
);
throw ValidationException::withMessages([
'email' => __('Zu viele Anfragen. Bitte versuchen Sie es in :minutes Minuten erneut.', [
'minutes' => max(1, ceil($seconds / 60)),
]),
]);
}
protected function throttleKey(): string
{
return 'magic-link|'.Str::transliterate(Str::lower($this->email).'|'.request()->ip());
}
protected function ipThrottleKey(): string
{
return 'magic-link-ip|'.request()->ip();
}
}; ?>
<div>
<x-session-status class="mb-4" />
<p class="text-[13.5px] text-ink-2 leading-[1.65] !-mt-2 mb-6">
{{ __('Geben Sie Ihre E-Mail-Adresse ein – wir senden Ihnen einen Anmeldelink, mit dem Sie sich ohne Passwort anmelden können. Das funktioniert für bestehende Konten ebenso wie für jede E-Mail, die bei einer') }}
<strong class="text-ink-2 font-semibold">{{ __('Firma') }}</strong> {{ __('oder als') }}
<strong class="text-ink-2 font-semibold">{{ __('Pressekontakt') }}</strong> {{ __('hinterlegt ist – Sie verwalten damit die Pressemitteilungen aller zugeordneten Firmen.') }}
</p>
<form wire:submit="requestLink" class="space-y-[18px]" novalidate>
<div>
<label class="field-label" for="magic-email">{{ __('E-Mail-Adresse') }}</label>
<input
id="magic-email"
type="email"
wire:model="email"
required
autofocus
autocomplete="email"
class="field-input"
placeholder="redaktion@ihr-unternehmen.de"
@error('email') aria-invalid="true" @enderror
/>
@error('email')
<p class="field-error">{{ $message }}</p>
@enderror
</div>
{{-- Honeypot: für Menschen unsichtbar --}}
<div class="hidden" aria-hidden="true">
<label for="magic-website">{{ __('Website') }}</label>
<input id="magic-website" type="text" wire:model="website" tabindex="-1" autocomplete="off" />
</div>
<button type="submit" class="auth-btn-primary !mt-[18px]" wire:loading.attr="disabled" wire:target="requestLink">
<span wire:loading.remove wire:target="requestLink">{{ __('Anmeldelink senden') }}</span>
<span wire:loading wire:target="requestLink">{{ __('Wird gesendet …') }}</span>
</button>
<a href="{{ route('login') }}" class="auth-btn-outline !mt-3" wire:navigate>
{{ __('Zurück zur Anmeldung') }}
</a>
</form>
</div>