Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/Livewire/Portal/Auth/ForgotPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace App\Livewire\Portal\Auth;

use App\Livewire\AbstractComponent;
use App\Models\User;
use App\Support\Facades\Flux;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;

#[Layout('portal::components.layouts.guest')]
class ForgotPassword extends AbstractComponent
{
public string $email = '';

public bool $linkSent = false;

/**
* Send password reset link.
*
* @throws ValidationException
*/
public function sendResetLink(): void
{
$this->validate([
'email' => ['required', 'email'],
]);

ResetPassword::createUrlUsing(fn (User $user, string $token): string => route('portal.password.reset', [
'token' => $token,
]));

$status = Password::sendResetLink(['email' => $this->email]);

if ($status === Password::RESET_LINK_SENT) {
$this->linkSent = true;
Flux::toastSuccess(__($status));

return;
}

throw ValidationException::withMessages([
'email' => __($status),
]);
}

public function render(): View
{
return view('portal::livewire.auth.forgot-password')
->title('Forgot Password');
}
}
92 changes: 92 additions & 0 deletions app/Livewire/Portal/Auth/ResetPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace App\Livewire\Portal\Auth;

use App\Livewire\AbstractComponent;
use App\Models\User;
use App\Support\Facades\Flux;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password as PasswordRule;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;

#[Layout('portal::components.layouts.guest')]
class ResetPassword extends AbstractComponent
{
public string $token = '';

public string $email = '';

public string $password = '';

public string $password_confirmation = '';

/**
* Initialize the component with the token and email from the URL.
*/
public function mount(string $token, ?string $email = null): void
{
$this->token = $token;
$this->email = $email ?? '';
}

/**
* Reset the user's password.
*
* @throws ValidationException
*/
public function resetPassword(): void
{
$this->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', PasswordRule::defaults()],
]);

$status = Password::reset(
[
'email' => $this->email,
'password' => $this->password,
'password_confirmation' => $this->password_confirmation,
'token' => $this->token,
],
function (User $user, string $password): void {
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();

event(new PasswordReset($user));
}
);

if ($status === Password::PASSWORD_RESET) {
Auth::attempt(['email' => $this->email, 'password' => $this->password], true);
Session::regenerate();

Flux::toastSuccess(__($status));

$this->redirect(route('portal.dashboard'), navigate: true);

return;
}

throw ValidationException::withMessages([
'email' => __($status),
]);
}

public function render(): View
{
return view('portal::livewire.auth.reset-password')
->title('Reset Password');
}
}
7 changes: 6 additions & 1 deletion app/Notifications/ResetPasswordNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ public function toMail(mixed $notifiable): MailMessage
#[Override]
protected function resetUrl(mixed $notifiable): string
{
// Check if a custom URL callback was set (e.g., by Portal)
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable, $this->token);
}

// Default: use localized route for web
return localized_route('localized.password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
]);
}
}
37 changes: 37 additions & 0 deletions resources/views/portal/livewire/auth/forgot-password.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div class="space-y-section">
<div class="text-center">
<flux:heading size="lg">Forgot your password?</flux:heading>
<flux:text class="mt-ui">
Enter your email address and we'll send you a link to reset your password.
</flux:text>
</div>

@if ($linkSent)
<flux:callout variant="success" icon="check-circle">
We have emailed your password reset link. Please check your inbox.
</flux:callout>

<div class="text-center">
<flux:link :href="route('portal.login')" wire:navigate>Back to login</flux:link>
</div>
@else
<form wire:submit="sendResetLink" class="space-y-section">
<flux:input
wire:model="email"
type="email"
label="Email"
placeholder="you@example.com"
required
autofocus
/>

<flux:button type="submit" variant="primary" class="w-full">
Send reset link
</flux:button>
</form>

<div class="text-center">
<flux:link :href="route('portal.login')" wire:navigate>Back to login</flux:link>
</div>
@endif
</div>
5 changes: 4 additions & 1 deletion resources/views/portal/livewire/auth/login.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
required
/>

<flux:checkbox wire:model="remember" label="Remember me" />
<div class="flex items-center justify-between">
<flux:checkbox wire:model="remember" label="Remember me" />
<flux:link :href="route('portal.password.request')" wire:navigate class="text-sm">Forgot password?</flux:link>
</div>

<flux:button type="submit" variant="primary" class="w-full">
Sign in
Expand Down
39 changes: 39 additions & 0 deletions resources/views/portal/livewire/auth/reset-password.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div class="space-y-section">
<div class="text-center">
<flux:heading size="lg">Reset your password</flux:heading>
<flux:text class="mt-ui">
Enter your new password below.
</flux:text>
</div>

<form wire:submit="resetPassword" class="space-y-section">
<flux:input
wire:model="email"
type="email"
label="Email"
placeholder="you@example.com"
required
autofocus
/>

<flux:input
wire:model="password"
type="password"
label="New Password"
placeholder="Your new password"
required
/>

<flux:input
wire:model="password_confirmation"
type="password"
label="Confirm Password"
placeholder="Confirm your new password"
required
/>

<flux:button type="submit" variant="primary" class="w-full">
Reset password
</flux:button>
</form>
</div>
4 changes: 4 additions & 0 deletions routes/portal.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
use App\Livewire\Portal\Admin\ApiUsage;
use App\Livewire\Portal\Admin\UserIndex;
use App\Livewire\Portal\Admin\UserShow;
use App\Livewire\Portal\Auth\ForgotPassword;
use App\Livewire\Portal\Auth\Login;
use App\Livewire\Portal\Auth\Register;
use App\Livewire\Portal\Auth\ResetPassword;
use App\Livewire\Portal\Auth\VerifyEmail;
use App\Livewire\Portal\Changelog;
use App\Livewire\Portal\Dashboard;
Expand Down Expand Up @@ -55,6 +57,8 @@
Route::middleware('guest')->group(function (): void {
Route::get('login', Login::class)->name('login');
Route::get('register', Register::class)->name('register');
Route::get('forgot-password', ForgotPassword::class)->name('password.request');
Route::get('reset-password/{token}', ResetPassword::class)->name('password.reset');
});

// Public routes (accessible to everyone)
Expand Down