E-Mail-Verifizierung (Entscheidung 15.06.): - User implementiert MustVerifyEmail; Registrierung legt inaktives, rollenloses Konto an und leitet auf die Danke-/Notice-Seite; Registered-Event versendet die Verifizierungsmail. Bestätigter Link aktiviert das Konto + vergibt customer-Rolle (ActivateUserAfterVerification). Backfill-Migration setzt email_verified_at für alle Bestands-User (sonst würde die verified-Middleware ~59k aktive Legacy-User aussperren). Seeder-User verifiziert. Auth-Flow-Korrekturen: - Magic-Link-Consume: rollensicherer Redirect ohne intended() (Customer landete sonst per stale intended=/dashboard im 403-Admin-Bereich). - Guest-Redirect (bootstrap/app.php) rollen-/verifizierungsbewusst statt fix /dashboard – schließt die 403-Sackgasse auf /login und /register. - Logout auf der Notice-Seite via echtes POST-Formular statt Livewire-Action (behebt 419 beim Session-Invalidate). - Magic-Link-Anforderung über eigenes Modal mit separater E-Mail-Eingabe. - Unverifizierte Login-Versuche landen auf der Notice-Seite. Sicherheitsfix Legacy-Rollen: - UserImporter mappte Alt-Gruppe 2 (Self-Publisher) auf editor (= Admin-Zugriff). Mapping auf customer korrigiert; Daten-Migration stuft die 65.950 fälschlichen Legacy-Editoren auf customer herab. Echte admin/api-only bleiben unberührt. Tests: Registration, EmailVerification, Authentication (Guest-Redirect), MagicLinkLogin (Modal/Redirect/Regression), Legacy-Import (Gruppen-Mapping). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
184 lines
7.1 KiB
PHP
184 lines
7.1 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Auth\Events\Registered;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Validation\Rules;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Konto erstellen', 'eyebrow' => 'Registrierung im Publisher-Hub', 'topRightLabel' => 'Bereits Konto?', 'topRightLinkText' => 'Anmelden', 'topRightLinkHref' => '/login'])] class extends Component {
|
|
public string $name = '';
|
|
|
|
public string $email = '';
|
|
|
|
public string $password = '';
|
|
|
|
public string $password_confirmation = '';
|
|
|
|
public bool $terms_accepted = false;
|
|
|
|
/**
|
|
* Handle an incoming registration request.
|
|
*/
|
|
public function register(): void
|
|
{
|
|
$validated = $this->validate([
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
|
'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
|
|
'terms_accepted' => ['accepted'],
|
|
], [
|
|
'terms_accepted.accepted' => 'Bitte bestätigen Sie unsere AGB und die Datenschutzerklärung.',
|
|
]);
|
|
|
|
unset($validated['terms_accepted']);
|
|
|
|
$validated['password'] = Hash::make($validated['password']);
|
|
// Konto bleibt bis zur E-Mail-Verifizierung inaktiv und rollenlos
|
|
// (Entscheidung 15.06.). Rolle + Aktivierung erfolgen erst nach dem
|
|
// bestätigten Verifizierungslink (ActivateUserAfterVerification).
|
|
$validated['is_active'] = false;
|
|
|
|
// Das Registered-Event versendet über den verdrahteten
|
|
// SendEmailVerificationNotification-Listener die Bestätigungsmail.
|
|
event(new Registered($user = User::create($validated)));
|
|
|
|
Auth::login($user);
|
|
|
|
// Direkt zur Bestätigungs-/Notice-Seite. Ohne navigate:true, weil das
|
|
// Panel ein anderes Vite-Bundle nutzt als das Hub-Auth-Layout.
|
|
$this->redirect(route('verification.notice', absolute: false));
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
@if (session('status'))
|
|
<div class="field-status mb-4" role="status">
|
|
{{ session('status') }}
|
|
</div>
|
|
@endif
|
|
|
|
<form wire:submit="register" class="space-y-[18px]" x-data="{ showPassword: false, showPasswordConfirmation: false }" novalidate>
|
|
|
|
<div>
|
|
<label class="field-label" for="auth-name">Name</label>
|
|
<input
|
|
id="auth-name"
|
|
type="text"
|
|
wire:model="name"
|
|
required
|
|
autofocus
|
|
autocomplete="name"
|
|
class="field-input"
|
|
placeholder="Vor- und Nachname"
|
|
@error('name') aria-invalid="true" @enderror
|
|
/>
|
|
@error('name')
|
|
<p class="field-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="field-label" for="auth-email">E-Mail-Adresse</label>
|
|
<input
|
|
id="auth-email"
|
|
type="email"
|
|
wire:model="email"
|
|
required
|
|
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>
|
|
|
|
<div>
|
|
<label class="field-label" for="auth-password">Passwort</label>
|
|
<div class="field-pw-wrap">
|
|
<input
|
|
id="auth-password"
|
|
wire:model="password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
required
|
|
autocomplete="new-password"
|
|
class="field-input pr-[72px]"
|
|
placeholder="Mindestens 8 Zeichen"
|
|
@error('password') aria-invalid="true" @enderror
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="field-affix"
|
|
@click="showPassword = !showPassword"
|
|
x-text="showPassword ? 'Verbergen' : 'Anzeigen'"
|
|
>Anzeigen</button>
|
|
</div>
|
|
@error('password')
|
|
<p class="field-error">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="field-label" for="auth-password-confirmation">Passwort bestätigen</label>
|
|
<div class="field-pw-wrap">
|
|
<input
|
|
id="auth-password-confirmation"
|
|
wire:model="password_confirmation"
|
|
:type="showPasswordConfirmation ? 'text' : 'password'"
|
|
required
|
|
autocomplete="new-password"
|
|
class="field-input pr-[72px]"
|
|
placeholder="Passwort wiederholen"
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="field-affix"
|
|
@click="showPasswordConfirmation = !showPasswordConfirmation"
|
|
x-text="showPasswordConfirmation ? 'Verbergen' : 'Anzeigen'"
|
|
>Anzeigen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="!mt-5">
|
|
<label for="auth-terms" class="flex items-start gap-3 cursor-pointer select-none">
|
|
<input
|
|
id="auth-terms"
|
|
type="checkbox"
|
|
wire:model="terms_accepted"
|
|
required
|
|
class="auth-check !mt-[3px]"
|
|
@error('terms_accepted') aria-invalid="true" @enderror
|
|
/>
|
|
<span class="text-[12.5px] text-ink-2 leading-[1.55]">
|
|
Ich habe die
|
|
<a href="{{ route('agb') }}" target="_blank" rel="noopener" class="link-hub">AGB</a>
|
|
und die
|
|
<a href="{{ route('datenschutz') }}" target="_blank" rel="noopener" class="link-hub">Datenschutzerklärung</a>
|
|
gelesen und stimme der Verarbeitung meiner Daten zur Konto-Erstellung ausdrücklich zu.
|
|
</span>
|
|
</label>
|
|
@error('terms_accepted')
|
|
<p class="field-error !ml-7">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<button type="submit" class="auth-btn-primary !mt-[18px]" wire:loading.attr="disabled" wire:target="register">
|
|
<span wire:loading.remove wire:target="register">Konto erstellen</span>
|
|
<span wire:loading wire:target="register">Konto wird angelegt …</span>
|
|
</button>
|
|
|
|
<div class="flex items-center gap-3 !mt-[22px] !mb-[14px]">
|
|
<span class="flex-1 h-px bg-bg-rule"></span>
|
|
<span class="text-[11px] font-semibold tracking-[0.18em] uppercase text-ink-3">Bereits Konto?</span>
|
|
<span class="flex-1 h-px bg-bg-rule"></span>
|
|
</div>
|
|
|
|
<a href="{{ route('login') }}" class="auth-btn-outline !mt-0" wire:navigate>
|
|
Stattdessen anmelden
|
|
</a>
|
|
</form>
|
|
</div>
|