diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..a300bfa --- /dev/null +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,30 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + /** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */ + $user = $request->user(); + + event(new Verified($user)); + } + + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } +} diff --git a/app/Livewire/Actions/Logout.php b/app/Livewire/Actions/Logout.php new file mode 100644 index 0000000..45993bb --- /dev/null +++ b/app/Livewire/Actions/Logout.php @@ -0,0 +1,22 @@ +logout(); + + Session::invalidate(); + Session::regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Livewire/Auth/ConfirmPassword.php b/app/Livewire/Auth/ConfirmPassword.php new file mode 100644 index 0000000..9a89db0 --- /dev/null +++ b/app/Livewire/Auth/ConfirmPassword.php @@ -0,0 +1,37 @@ +validate([ + 'password' => ['required', 'string'], + ]); + + if (! Auth::guard('web')->validate([ + 'email' => Auth::user()->email, + 'password' => $this->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + session(['auth.password_confirmed_at' => time()]); + + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + } +} diff --git a/app/Livewire/Auth/ForgotPassword.php b/app/Livewire/Auth/ForgotPassword.php new file mode 100644 index 0000000..7f3b681 --- /dev/null +++ b/app/Livewire/Auth/ForgotPassword.php @@ -0,0 +1,27 @@ +validate([ + 'email' => ['required', 'string', 'email'], + ]); + + Password::sendResetLink($this->only('email')); + + session()->flash('status', __('A reset link will be sent if the account exists.')); + } +} diff --git a/app/Livewire/Auth/Login.php b/app/Livewire/Auth/Login.php new file mode 100644 index 0000000..9925f63 --- /dev/null +++ b/app/Livewire/Auth/Login.php @@ -0,0 +1,77 @@ +validate(); + + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + Session::regenerate(); + + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + } + + /** + * Ensure the authentication request is not rate limited. + */ + protected function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout(request())); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the authentication rate limiting throttle key. + */ + protected function throttleKey(): string + { + return Str::transliterate(Str::lower($this->email).'|'.request()->ip()); + } +} diff --git a/app/Livewire/Auth/Register.php b/app/Livewire/Auth/Register.php new file mode 100644 index 0000000..8541536 --- /dev/null +++ b/app/Livewire/Auth/Register.php @@ -0,0 +1,43 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], + 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], + ]); + + $validated['password'] = Hash::make($validated['password']); + + event(new Registered(($user = User::create($validated)))); + + Auth::login($user); + + $this->redirect(route('dashboard', absolute: false), navigate: true); + } +} diff --git a/app/Livewire/Auth/ResetPassword.php b/app/Livewire/Auth/ResetPassword.php new file mode 100644 index 0000000..28b3940 --- /dev/null +++ b/app/Livewire/Auth/ResetPassword.php @@ -0,0 +1,76 @@ +token = $token; + + $this->email = request()->string('email'); + } + + /** + * Reset the password for the given user. + */ + public function resetPassword(): void + { + $this->validate([ + 'token' => ['required'], + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $this->only('email', 'password', 'password_confirmation', 'token'), + function ($user) { + $user->forceFill([ + 'password' => Hash::make($this->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + if ($status != Password::PasswordReset) { + $this->addError('email', __($status)); + + return; + } + + Session::flash('status', __($status)); + + $this->redirectRoute('login', navigate: true); + } +} diff --git a/app/Livewire/Auth/VerifyEmail.php b/app/Livewire/Auth/VerifyEmail.php new file mode 100644 index 0000000..ba11d73 --- /dev/null +++ b/app/Livewire/Auth/VerifyEmail.php @@ -0,0 +1,39 @@ +hasVerifiedEmail()) { + $this->redirectIntended(default: route('dashboard', absolute: false), navigate: true); + + return; + } + + Auth::user()->sendEmailVerificationNotification(); + + Session::flash('status', 'verification-link-sent'); + } + + /** + * Log the current user out of the application. + */ + public function logout(Logout $logout): void + { + $logout(); + + $this->redirect('/', navigate: true); + } +} diff --git a/app/Livewire/Dashboard/Dashboard.php b/app/Livewire/Dashboard/Dashboard.php new file mode 100644 index 0000000..1e0ef9a --- /dev/null +++ b/app/Livewire/Dashboard/Dashboard.php @@ -0,0 +1,13 @@ +layout('components.layouts.dashboard'); + } +} diff --git a/app/Livewire/Settings/Appearance.php b/app/Livewire/Settings/Appearance.php new file mode 100644 index 0000000..be0927a --- /dev/null +++ b/app/Livewire/Settings/Appearance.php @@ -0,0 +1,12 @@ +validate([ + 'password' => ['required', 'string', 'current_password'], + ]); + + tap(Auth::user(), $logout(...))->delete(); + + $this->redirect('/', navigate: true); + } +} diff --git a/app/Livewire/Settings/Password.php b/app/Livewire/Settings/Password.php new file mode 100644 index 0000000..9fb0d26 --- /dev/null +++ b/app/Livewire/Settings/Password.php @@ -0,0 +1,45 @@ +validate([ + 'current_password' => ['required', 'string', 'current_password'], + 'password' => ['required', 'string', PasswordRule::defaults(), 'confirmed'], + ]); + } catch (ValidationException $e) { + $this->reset('current_password', 'password', 'password_confirmation'); + + throw $e; + } + + Auth::user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + $this->reset('current_password', 'password', 'password_confirmation'); + + $this->dispatch('password-updated'); + } +} diff --git a/app/Livewire/Settings/Profile.php b/app/Livewire/Settings/Profile.php new file mode 100644 index 0000000..d114ad2 --- /dev/null +++ b/app/Livewire/Settings/Profile.php @@ -0,0 +1,76 @@ +name = Auth::user()->name; + $this->email = Auth::user()->email; + } + + /** + * Update the profile information for the currently authenticated user. + */ + public function updateProfileInformation(): void + { + $user = Auth::user(); + + $validated = $this->validate([ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'lowercase', + 'email', + 'max:255', + Rule::unique(User::class)->ignore($user->id), + ], + ]); + + $user->fill($validated); + + if ($user->isDirty('email')) { + $user->email_verified_at = null; + } + + $user->save(); + + $this->dispatch('profile-updated', name: $user->name); + } + + /** + * Send an email verification notification to the current user. + */ + public function resendVerificationNotification(): void + { + $user = Auth::user(); + + if ($user->hasVerifiedEmail()) { + $this->redirectIntended(default: route('dashboard', absolute: false)); + + return; + } + + $user->sendEmailVerificationNotification(); + + Session::flash('status', 'verification-link-sent'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 81e5ffe..3a0508e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,8 +8,10 @@ use Filament\Panel; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Contracts\Auth\MustVerifyEmail; +use Illuminate\Support\Str; -class User extends Authenticatable implements FilamentUser +class User extends Authenticatable implements FilamentUser, MustVerifyEmail { /** @use HasFactory<\Database\Factories\UserFactory> */ use HasFactory, Notifiable; @@ -48,6 +50,14 @@ class User extends Authenticatable implements FilamentUser ]; } + public function initials(): string + { + return Str::of($this->name) + ->explode(' ') + ->map(fn (string $name) => Str::of($name)->substr(0, 1)) + ->implode(''); + } + public function canAccessPanel(Panel $panel): bool { return str_ends_with($this->email, '@zemail.me') && $this->level === 9 && $this->hasVerifiedEmail(); diff --git a/resources/views/components/action-message.blade.php b/resources/views/components/action-message.blade.php new file mode 100644 index 0000000..d313ee6 --- /dev/null +++ b/resources/views/components/action-message.blade.php @@ -0,0 +1,14 @@ +@props([ + 'on', +]) + +
diff --git a/resources/views/components/app-logo-icon.blade.php b/resources/views/components/app-logo-icon.blade.php new file mode 100644 index 0000000..a76b040 --- /dev/null +++ b/resources/views/components/app-logo-icon.blade.php @@ -0,0 +1 @@ +
diff --git a/resources/views/components/auth-header.blade.php b/resources/views/components/auth-header.blade.php
new file mode 100644
index 0000000..e596a3f
--- /dev/null
+++ b/resources/views/components/auth-header.blade.php
@@ -0,0 +1,9 @@
+@props([
+ 'title',
+ 'description',
+])
+
+