presseportale/resources/views/livewire/auth/magic-link.blade.php
Kevin Adametz 1ac5badd53 i18n: Auth-Views vollstaendig uebersetzbar machen (Readiness)
Die Auth-Strecke (Login, Registrierung, Passwort-Reset/-Bestaetigung,
Magic-Link, E-Mail-Verifizierung, 2FA) war als einziger Backend-Bereich
noch nicht durchgaengig __()-gewrappt. Alle sichtbaren deutschen Strings
in __() gewickelt (Deutsch bleibt Schluessel). Eingebettete Links (AGB/
Datenschutz) und Alpine-x-text sauber segmentiert. Heading/Eyebrow und
topRight-Label/-Link sowie der From-Banner werden jetzt im Auth-Layout am
Ausgabepunkt uebersetzt (im #[Layout]-Attribut sind keine __()-Aufrufe
moeglich). Leeres lang/en.json als Geruest fuer die spaetere EN-Befuellung.

Kunden-, Admin-, Settings- und Component-Views waren bereits vollstaendig
gewrappt (per Audit bestaetigt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:00:20 +00:00

133 lines
5 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>
@if (session('status'))
<div class="field-status mb-4" role="status">
{{ session('status') }}
</div>
@endif
<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>