diff --git a/app/Actions/PasswordResetAction.php b/app/Actions/PasswordResetAction.php new file mode 100644 index 0000000..b8a152b --- /dev/null +++ b/app/Actions/PasswordResetAction.php @@ -0,0 +1,24 @@ +first(); + throw_if(! $user, new UserNotFoundException('User not found')); + + $otp = $this->otpService->generate($user); + $this->mailAction->execute($user->email, $otp); + } +} diff --git a/app/Actions/SendPasswordResetMailAction.php b/app/Actions/SendPasswordResetMailAction.php new file mode 100644 index 0000000..2fe064d --- /dev/null +++ b/app/Actions/SendPasswordResetMailAction.php @@ -0,0 +1,21 @@ +send(new PasswordResetMail($otp)); + \Log::info('Mail sent successfully', ['message' => $message]); + } catch (\Throwable $e) { + \Log::info('Mail send failed', ['message' => $e->getMessage()]); + } + } +} diff --git a/app/Exceptions/UserNotFoundException.php b/app/Exceptions/UserNotFoundException.php new file mode 100644 index 0000000..d99e442 --- /dev/null +++ b/app/Exceptions/UserNotFoundException.php @@ -0,0 +1,7 @@ +validate([ + 'email' => 'required|email', + ]); + + try { + $action->execute($data); + + return to_route('password.reset.show.verify')->with('success', 'Password reset code is sent'); + } catch (UserNotFoundException $e) { + return to_route('password.reset.show.verify')->with('success', 'Password reset code is sent'); + } + } + + public function showVerify() + { + return view('auth.passwords.verify'); + } + + public function verify(Request $request, OTPService $otpService) + { + $data = $request->validate(['otp' => 'required|string:min:5:max:6']); + try { + $isVerified = $otpService->verify($data['otp']); + if (! $isVerified) { + return back()->with('error', 'Invalid OTP'); + } + + return to_route('password.reset.show.update')->with('success', 'OTP Verified'); + } catch (UserNotFoundException $e) { + return back()->with('error', 'Session Expired'); + } + } + + public function showUpdate() + { + return view('auth.passwords.update'); + } + + public function update(Request $request) + { + $data = $request->validate(['password' => 'required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()]); + $user = User::find(Session::get('user_id')); + if (! $user) { + return back()->with('error', 'Session Expired'); + } + $user->update(['password' => $data['password']]); + + return to_route('login.create')->with('success', 'Password updated successfully'); + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 20d9fda..f6aef36 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -45,7 +45,7 @@ public function store(StoreRegisterdUser $request) }); return to_route('login.create') - ->with('userRegistered', 'User registered successfully.'); + ->with('success', 'User registered successfully.'); } catch (\Throwable $e) { Log::error('Registration Failed: '.$e->getMessage()); diff --git a/app/Mail/PasswordResetMail.php b/app/Mail/PasswordResetMail.php new file mode 100644 index 0000000..32fea89 --- /dev/null +++ b/app/Mail/PasswordResetMail.php @@ -0,0 +1,38 @@ + $this->otp] + ); + } + + public function attachments(): array + { + return []; + } +} diff --git a/app/Services/OTPService.php b/app/Services/OTPService.php new file mode 100644 index 0000000..6fc49e1 --- /dev/null +++ b/app/Services/OTPService.php @@ -0,0 +1,42 @@ +id", $code, now()->addMinutes(10)); + Session::put('user_id', $user->id); + + return $code; + } + + /** + * @throws \Exception + */ + public function verify(string $otp): bool + { + $user = User::find(Session::get('user_id')); + + if (! $user) { + throw new UserNotFoundException('User not found'); + } + + $code = Cache::get("otp_$user->id"); + if ($code === $otp) { + Cache::forget("otp_$user->id"); + + return true; + } + + return false; + } +} diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 9a70de9..18c0650 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -20,7 +20,7 @@ class="bg-linear-135 h-screen from-[#EFF6FF] to-[#FCF3F8] flex flex-col justify- @endsession - @session('userRegistered') + @session('success') {{$value}} @@ -35,7 +35,7 @@ class="bg-linear-135 h-screen from-[#EFF6FF] to-[#FCF3F8] flex flex-col justify- - Forgot password? + Forgot password? Sign In diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php new file mode 100644 index 0000000..aa4021c --- /dev/null +++ b/resources/views/auth/passwords/reset.blade.php @@ -0,0 +1,36 @@ + +
+ + +
+ +

Reset Password

+
+ + @session('error') + + {{$value}} + + @endsession + + @session('success') + + {{$value}} + + @endsession + +
+ @csrf + + Reset Password + + +
+
+
diff --git a/resources/views/auth/passwords/update.blade.php b/resources/views/auth/passwords/update.blade.php new file mode 100644 index 0000000..c505a88 --- /dev/null +++ b/resources/views/auth/passwords/update.blade.php @@ -0,0 +1,37 @@ + +
+ + +
+ +

Reset Password

+
+ + @session('error') + + {{$value}} + + @endsession + + @session('success') + + {{$value}} + + @endsession + +
+ @csrf + + + Reset Password + + +
+
+
diff --git a/resources/views/auth/passwords/verify.blade.php b/resources/views/auth/passwords/verify.blade.php new file mode 100644 index 0000000..32414de --- /dev/null +++ b/resources/views/auth/passwords/verify.blade.php @@ -0,0 +1,36 @@ + +
+ + +
+ +

Verify OTP

+
+ + @session('error') + + {{$value}} + + @endsession + + @session('success') + + {{$value}} + + @endsession + +
+ @csrf + + Reset Password + + +
+
+
diff --git a/resources/views/components/layout.blade.php b/resources/views/components/layout.blade.php index 7364cfd..9b37870 100644 --- a/resources/views/components/layout.blade.php +++ b/resources/views/components/layout.blade.php @@ -14,7 +14,7 @@ {{ $pageTitle }} - + diff --git a/resources/views/components/ui/input.blade.php b/resources/views/components/ui/input.blade.php index cc7f5d9..1ad9a44 100644 --- a/resources/views/components/ui/input.blade.php +++ b/resources/views/components/ui/input.blade.php @@ -16,7 +16,7 @@ > {{$slot}} @if($description !== '') -

{{$description}}

+

{{$description}}

@endif diff --git a/resources/views/emails/password-reset.blade.php b/resources/views/emails/password-reset.blade.php new file mode 100644 index 0000000..309abc8 --- /dev/null +++ b/resources/views/emails/password-reset.blade.php @@ -0,0 +1,9 @@ + + + # OTP: {{$otp}} + + You are receiving this email because we received a password reset request for your account. + If you did not request a password reset, no further action is required. + Thanks, + {{ config('app.name') }} + diff --git a/routes/web/auth.php b/routes/web/auth.php index 6b5f63f..3f73b81 100644 --- a/routes/web/auth.php +++ b/routes/web/auth.php @@ -3,6 +3,7 @@ use App\Enums\UserTypes; use App\Http\Controllers\Auth\AuthenticatedUserController; use App\Http\Controllers\Auth\ImpersonatedUserController; +use App\Http\Controllers\Auth\PasswordResetController; use App\Http\Controllers\Auth\RegisteredUserController; use App\Http\Middleware\HasRole; @@ -11,6 +12,21 @@ ->middleware('throttle:6,1') ->only(['create', 'store']); Route::resource('/register', RegisteredUserController::class)->only(['create', 'store']); + + Route::prefix('password/reset')->name('password.reset.')->middleware('throttle:20,1')->group(function () { + Route::controller(PasswordResetController::class)->group(function () { + Route::get('/', 'show')->name('show'); + Route::post('/', 'sendCode')->name('send'); + Route::get('/verify', + 'showVerify')->name('show.verify'); + Route::post('/verify', + 'verify')->name('verify'); + Route::get('/update', + 'showUpdate')->name('show.update'); + Route::post('/update', + 'update')->name('update'); + }); + }); }); Route::delete('/logout', [AuthenticatedUserController::class, 'destroy'])