diff --git a/.env.example b/.env.example index a076200..f16d629 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,6 @@ VITE_APP_NAME="${APP_NAME}" TWILIO_SID= TWILIO_AUTH_TOKEN= TWILIO_NUMBER= + +# OTP valid time in minutes +OTP_LIFESPAN=10 diff --git a/app/Actions/PasswordReset/ResendOTPAction.php b/app/Actions/PasswordReset/ResendOTPAction.php new file mode 100644 index 0000000..14cfda1 --- /dev/null +++ b/app/Actions/PasswordReset/ResendOTPAction.php @@ -0,0 +1,29 @@ +otpService->generate($user); + + $this->otpToUserAction->execute(['user' => $user, 'otp' => $otp]); + + } +} diff --git a/app/Actions/PasswordReset/SendOTPAction.php b/app/Actions/PasswordReset/SendOTPAction.php new file mode 100644 index 0000000..3c381c1 --- /dev/null +++ b/app/Actions/PasswordReset/SendOTPAction.php @@ -0,0 +1,30 @@ +first(); + throw_if(! $user, new UserNotFoundException('User not found')); + + \Session::put('otp_user_id', $user->id); + $otp = $this->otpService->generate($user); + + $this->otpToUserAction->execute(['user' => $user, 'otp' => $otp]); + + } +} diff --git a/app/Actions/PasswordResetAction.php b/app/Actions/PasswordReset/SendOTPToUserAction.php similarity index 63% rename from app/Actions/PasswordResetAction.php rename to app/Actions/PasswordReset/SendOTPToUserAction.php index 320e294..489fc62 100644 --- a/app/Actions/PasswordResetAction.php +++ b/app/Actions/PasswordReset/SendOTPToUserAction.php @@ -1,30 +1,27 @@ $data + * * @throws \Throwable */ public function execute(array $data): void { - $user = User::where('email', $data['email'])->first(); - throw_if(! $user, new UserNotFoundException('User not found')); + ['user' => $user,'otp' => $otp] = $data; - $otp = $this->otpService->generate($user); $this->mailAction->execute($user->email, $otp); if ($user?->type->phone !== null) { diff --git a/app/Actions/PasswordReset/VerifyOTPAction.php b/app/Actions/PasswordReset/VerifyOTPAction.php new file mode 100644 index 0000000..c9223c9 --- /dev/null +++ b/app/Actions/PasswordReset/VerifyOTPAction.php @@ -0,0 +1,30 @@ +otpService->verify($user, $data['otp']); + if ($isVerified) { + \Session::forget('otp_user_id'); + } + + return $isVerified; + } +} diff --git a/app/Http/Controllers/Auth/PasswordResetController.php b/app/Http/Controllers/Auth/PasswordResetController.php index b3821f1..37f7f80 100644 --- a/app/Http/Controllers/Auth/PasswordResetController.php +++ b/app/Http/Controllers/Auth/PasswordResetController.php @@ -2,11 +2,12 @@ namespace App\Http\Controllers\Auth; -use App\Actions\PasswordResetAction; +use App\Actions\PasswordReset\ResendOTPAction; +use App\Actions\PasswordReset\SendOTPAction; +use App\Actions\PasswordReset\VerifyOTPAction; use App\Exceptions\UserNotFoundException; use App\Http\Controllers\Controller; use App\Models\User; -use App\Services\OTPService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; use Illuminate\Validation\Rules\Password; @@ -18,7 +19,7 @@ public function show() return view('auth.passwords.reset'); } - public function sendCode(Request $request, PasswordResetAction $action) + public function sendCode(Request $request, SendOTPAction $action) { $data = $request->validate([ 'email' => 'required|email', @@ -27,7 +28,8 @@ public function sendCode(Request $request, PasswordResetAction $action) try { $action->execute($data); - return to_route('password.reset.show.verify')->with('success', 'Password reset code is sent'); + 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'); } @@ -35,14 +37,15 @@ public function sendCode(Request $request, PasswordResetAction $action) public function showVerify() { - return view('auth.passwords.verify'); + return view('auth.passwords.verify') + ->with('expiryMinutes', 3); } - public function verify(Request $request, OTPService $otpService) + public function verify(Request $request, VerifyOTPAction $otpAction) { $data = $request->validate(['otp' => 'required|string:min:5:max:6']); try { - $isVerified = $otpService->verify($data['otp']); + $isVerified = $otpAction->execute($data); if (! $isVerified) { return back()->with('error', 'Invalid OTP'); } @@ -60,7 +63,9 @@ public function showUpdate() public function update(Request $request) { - $data = $request->validate(['password' => 'required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()]); + $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'); @@ -69,4 +74,16 @@ public function update(Request $request) return to_route('login.create')->with('success', 'Password updated successfully'); } + + public function resend(ResendOTPAction $otpAction) + { + try { + $otpAction->execute(); + + 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'); + } + } } diff --git a/app/Services/OTPService.php b/app/Services/OTPService.php index 6fc49e1..7ffd39d 100644 --- a/app/Services/OTPService.php +++ b/app/Services/OTPService.php @@ -2,34 +2,21 @@ namespace App\Services; -use App\Exceptions\UserNotFoundException; use App\Models\User; use Illuminate\Support\Facades\Cache; -use Session; class OTPService { public function generate(User $user, int $length = 6): string { $code = \Str::random($length); - - Cache::put("otp_$user->id", $code, now()->addMinutes(10)); - Session::put('user_id', $user->id); + Cache::put("otp_$user->id", $code, now()->addMinutes((int) config('auth.otp_lifespan', '10'))); return $code; } - /** - * @throws \Exception - */ - public function verify(string $otp): bool + public function verify(User $user, 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"); diff --git a/config/auth.php b/config/auth.php index 7d1eb0d..07d27f8 100644 --- a/config/auth.php +++ b/config/auth.php @@ -111,5 +111,6 @@ */ 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + 'otp_lifespan' => env('OTP_LIFESPAN', 10), ]; diff --git a/resources/views/auth/passwords/verify.blade.php b/resources/views/auth/passwords/verify.blade.php index 2044d14..a486aa6 100644 --- a/resources/views/auth/passwords/verify.blade.php +++ b/resources/views/auth/passwords/verify.blade.php @@ -1,6 +1,5 @@ -
+
+ + diff --git a/resources/views/emails/password-reset.blade.php b/resources/views/emails/password-reset.blade.php index 309abc8..2155518 100644 --- a/resources/views/emails/password-reset.blade.php +++ b/resources/views/emails/password-reset.blade.php @@ -1,9 +1,102 @@ - + + + + + + OTP Email + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + +
+

DH

+

{{config('app.name')}}

+
+ {{ now()->format('d M, Y') }} +
+
+ +

Your OTP

+ +

+ Thank you for choosing {{config('app.name')}}. Use the following OTP to complete the + procedure to change your reset your password. OTP is valid for + {{config('auth.otp_lifespan')}} minutes. Do not share this code with + others. +

+ +
+ @foreach(str_split((string)$otp) as $digit) + + {{ $digit }} + + @endforeach +
+ +
+
+ + + diff --git a/routes/web/auth.php b/routes/web/auth.php index 3f73b81..0f60b23 100644 --- a/routes/web/auth.php +++ b/routes/web/auth.php @@ -16,15 +16,22 @@ 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::get('/resend', 'resend')->name('resend'); }); }); });