fix(auth): toaster on auth pages + audit email-link resets

Codex review follow-ups: extract the notify toaster into a shared partial and
include it in the auth layout too, so sendResetLink + recovery-code regenerate
actually surface their confirmation (the auth layout had no toaster). Record an
AuditEvent for password resets via the email-token path, matching the 2FA path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-14 17:07:52 +02:00
parent 3a67aa99ec
commit 2ff2cc34c0
4 changed files with 34 additions and 22 deletions

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Auth; namespace App\Livewire\Auth;
use App\Models\AuditEvent;
use Illuminate\Auth\Events\PasswordReset; use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
@ -50,6 +51,14 @@ class ResetPassword extends Component
'remember_token' => Str::random(60), 'remember_token' => Str::random(60),
])->save(); ])->save();
AuditEvent::create([
'user_id' => $user->id,
'actor' => $user->name ?? 'system',
'action' => 'password.reset',
'target' => $user->email,
'ip' => request()->ip(),
]);
event(new PasswordReset($user)); event(new PasswordReset($user));
}, },
); );

View File

@ -40,28 +40,7 @@
</main> </main>
</div> </div>
</div> </div>
{{-- Toaster: catches Livewire-dispatched `notify` browser events --}} @include('partials.toaster')
<div
x-data="{ toasts: [] }"
x-on:notify.window="
const msg = $event.detail?.message ?? $event.detail?.[0]?.message ?? @js(__('shell.toast_done'));
const id = (window.crypto?.randomUUID?.() ?? String(Date.now() + Math.random()));
toasts.push({ id, msg });
setTimeout(() => { toasts = toasts.filter(t => t.id !== id) }, 4000);
"
class="pointer-events-none fixed inset-x-0 bottom-4 z-50 flex flex-col items-center gap-2 px-4 sm:items-end sm:px-6"
aria-live="polite"
>
<template x-for="t in toasts" :key="t.id">
<div
x-transition.opacity.duration.200ms
class="pointer-events-auto flex items-center gap-2.5 rounded-md border border-line bg-raised px-4 py-2.5 shadow-pop"
>
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-online"></span>
<span class="font-mono text-xs text-ink-2" x-text="t.msg"></span>
</div>
</template>
</div>
{{-- Command palette + keyboard shortcuts (persistent; one global keydown listener) --}} {{-- Command palette + keyboard shortcuts (persistent; one global keydown listener) --}}
<x-command-palette /> <x-command-palette />

View File

@ -78,6 +78,7 @@
</div> </div>
</main> </main>
</div> </div>
@include('partials.toaster')
@livewireScripts @livewireScripts
</body> </body>
</html> </html>

View File

@ -0,0 +1,23 @@
{{-- Toaster: catches Livewire-dispatched `notify` browser events. Shared by the app
and auth layouts so notifications surface on every page. --}}
<div
x-data="{ toasts: [] }"
x-on:notify.window="
const msg = $event.detail?.message ?? $event.detail?.[0]?.message ?? @js(__('shell.toast_done'));
const id = (window.crypto?.randomUUID?.() ?? String(Date.now() + Math.random()));
toasts.push({ id, msg });
setTimeout(() => { toasts = toasts.filter(t => t.id !== id) }, 4000);
"
class="pointer-events-none fixed inset-x-0 bottom-4 z-50 flex flex-col items-center gap-2 px-4 sm:items-end sm:px-6"
aria-live="polite"
>
<template x-for="t in toasts" :key="t.id">
<div
x-transition.opacity.duration.200ms
class="pointer-events-auto flex items-center gap-2.5 rounded-md border border-line bg-raised px-4 py-2.5 shadow-pop"
>
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-online"></span>
<span class="font-mono text-xs text-ink-2" x-text="t.msg"></span>
</div>
</template>
</div>