- Security-Header-Middleware (nosniff, X-Frame-Options, Referrer-/ Permissions-Policy) global, proxy-unabhaengig; CSP bewusst separat. - Firmenlogo-Upload im Create-Formular durch den ImageService-Pfad ersetzt (Mime-Whitelist ohne SVG, Re-Encode, UUID-Name) - vorher rohe store()-Ablage ohne Server-Validierung (SVG/XSS moeglich). - Rate-Limits ergaenzt: Passwort-Reset (3/E-Mail + 15/IP pro Std.), Registrierung (10/IP pro Std.), throttle auf magic-login- und pm-vorschau-Consume-Routen (Defense-in-Depth). - EnsureUserIsCustomer als persistente Livewire-Middleware (symmetrisch zum Admin-Gate, greift auch bei Folge-Requests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.5 KiB
PHP
95 lines
3.5 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Password;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Passwort vergessen', 'eyebrow' => 'Passwort zurücksetzen', 'topRightLabel' => 'Doch erinnert?', 'topRightLinkText' => 'Zur Anmeldung', 'topRightLinkHref' => '/login'])] class extends Component {
|
|
public string $email = '';
|
|
|
|
/**
|
|
* Send a password reset link to the provided email address.
|
|
*/
|
|
public function sendPasswordResetLink(): void
|
|
{
|
|
$this->validate([
|
|
'email' => ['required', 'string', 'email'],
|
|
]);
|
|
|
|
// Drosseln wie beim Anmeldelink: pro E-Mail (Mail-Fluten eines
|
|
// Postfachs) und pro IP (Durchprobieren vieler Adressen). Der
|
|
// Password-Broker drosselt zusätzlich nur pro Nutzer (60s).
|
|
$this->ensureIsNotRateLimited();
|
|
RateLimiter::hit($this->throttleKey(), 3600);
|
|
RateLimiter::hit($this->ipThrottleKey(), 3600);
|
|
|
|
Password::sendResetLink($this->only('email'));
|
|
|
|
session()->flash('status', __('A reset link will be sent if the account exists.'));
|
|
}
|
|
|
|
protected function ensureIsNotRateLimited(): void
|
|
{
|
|
$withinEmail = ! RateLimiter::tooManyAttempts($this->throttleKey(), 3);
|
|
$withinIp = ! RateLimiter::tooManyAttempts($this->ipThrottleKey(), 15);
|
|
|
|
if ($withinEmail && $withinIp) {
|
|
return;
|
|
}
|
|
|
|
throw ValidationException::withMessages([
|
|
'email' => __('Zu viele Anfragen. Bitte versuchen Sie es später erneut.'),
|
|
]);
|
|
}
|
|
|
|
protected function throttleKey(): string
|
|
{
|
|
return 'password-reset|'.Str::transliterate(Str::lower($this->email));
|
|
}
|
|
|
|
protected function ipThrottleKey(): string
|
|
{
|
|
return 'password-reset-ip|'.request()->ip();
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
<p class="text-[13.5px] text-ink-2 leading-[1.6] !-mt-4 mb-6">
|
|
{{ __('Geben Sie die E-Mail-Adresse Ihres Kontos ein. Sie erhalten innerhalb weniger Minuten einen Link, mit dem Sie ein neues Passwort vergeben können.') }}
|
|
</p>
|
|
|
|
<x-session-status class="mb-4" />
|
|
|
|
<form wire:submit="sendPasswordResetLink" class="space-y-[18px]" novalidate>
|
|
|
|
<div>
|
|
<label class="field-label" for="auth-email">{{ __('E-Mail-Adresse') }}</label>
|
|
<input
|
|
id="auth-email"
|
|
type="email"
|
|
wire:model="email"
|
|
required
|
|
autofocus
|
|
autocomplete="username"
|
|
class="field-input"
|
|
placeholder="redaktion@ihr-unternehmen.de"
|
|
@error('email') aria-invalid="true" @enderror
|
|
/>
|
|
@error('email')
|
|
<p class="field-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<button type="submit" class="auth-btn-primary !mt-[22px]" wire:loading.attr="disabled" wire:target="sendPasswordResetLink">
|
|
<span wire:loading.remove wire:target="sendPasswordResetLink">{{ __('Reset-Link senden') }}</span>
|
|
<span wire:loading wire:target="sendPasswordResetLink">{{ __('Link wird gesendet …') }}</span>
|
|
</button>
|
|
|
|
<div class="flex items-center justify-center pt-2 text-[12.5px] text-ink-3">
|
|
<a href="{{ route('login') }}" class="link-hub" wire:navigate>← {{ __('Zurück zur Anmeldung') }}</a>
|
|
</div>
|
|
</form>
|
|
</div>
|