diff --git a/app/Actions/Auth/ChangePasswordAction.php b/app/Actions/Auth/ChangePasswordAction.php new file mode 100644 index 0000000..42c2c24 --- /dev/null +++ b/app/Actions/Auth/ChangePasswordAction.php @@ -0,0 +1,25 @@ +password)) { + return false; + } + + $hashedPassword = Hash::make($newPassword); + + $user->password = $hashedPassword; + $user->save(); + + return true; + } +} diff --git a/app/Actions/Auth/GenerateResetCodeAction.php b/app/Actions/Auth/GenerateResetCodeAction.php new file mode 100644 index 0000000..035dd16 --- /dev/null +++ b/app/Actions/Auth/GenerateResetCodeAction.php @@ -0,0 +1,36 @@ +updateOrInsert( + ["email" => $email], + [ + "email" => $email, + "token" => Hash::make($code), + "created_at" => now(), + ], + ); + + return $code; + } +} diff --git a/app/Actions/Auth/ResetPasswordAction.php b/app/Actions/Auth/ResetPasswordAction.php new file mode 100644 index 0000000..e7d333d --- /dev/null +++ b/app/Actions/Auth/ResetPasswordAction.php @@ -0,0 +1,29 @@ +forceFill([ + "password" => Hash::make($password), + ])->setRememberToken(Str::random(60)); + + $user->save(); + + event(new PasswordReset($user)); + }); + + return $status === Password::PASSWORD_RESET; + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..bd02ff9 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,31 @@ +validated(); + + if (!Auth::attempt($credentials)) { + return response()->json([], Response::HTTP_FORBIDDEN); + } + + $user = Auth::user(); + $token = $user->createToken("api-token")->plainTextToken; + + return response()->json([ + "token" => $token, + "user_id" => $user->id, + ], Response::HTTP_OK); + } +} diff --git a/app/Http/Controllers/Auth/LogoutController.php b/app/Http/Controllers/Auth/LogoutController.php new file mode 100644 index 0000000..64690df --- /dev/null +++ b/app/Http/Controllers/Auth/LogoutController.php @@ -0,0 +1,25 @@ +user(); + $token = $user->currentAccessToken(); + + if ($token) { + $token->delete(); + } + + return response()->json([], Response::HTTP_OK); + } +} diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php new file mode 100644 index 0000000..a94a608 --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordController.php @@ -0,0 +1,62 @@ +validated(); + $email = $validated["email"]; + + $code = $generateResetCodeAction->execute($email); + + $user = User::query()->where("email", $email)->first(); + $user?->notify(new ForgotPasswordNotification($code)); + + return response()->json([], Response::HTTP_OK); + } + + public function resetPassword(ResetPasswordRequest $request, ResetPasswordAction $resetPasswordAction): JsonResponse + { + $validated = $request->validated(); + $success = $resetPasswordAction->execute($validated); + + return $success + ? response()->json([], Response::HTTP_OK) + : response()->json([], Response::HTTP_BAD_REQUEST); + } + + public function changePassword(ChangePasswordRequest $request, ChangePasswordAction $changePasswordAction): JsonResponse + { + $user = $request->user(); + $validated = $request->validated(); + + $currentPassword = $validated["current_password"]; + $newPassword = $validated["password"]; + + $success = $changePasswordAction->execute( + $user, + $currentPassword, + $newPassword, + ); + + return $success + ? response()->json([], Response::HTTP_OK) + : response()->json([], Response::HTTP_FORBIDDEN); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 0000000..89f6c9b --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,26 @@ +validated(); + + $user = new User($validated); + $user->password = Hash::make($validated["password"]); + $user->save(); + + return response()->json([], Response::HTTP_CREATED); + } +} diff --git a/app/Http/Requests/Auth/ChangePasswordRequest.php b/app/Http/Requests/Auth/ChangePasswordRequest.php new file mode 100644 index 0000000..99fd31f --- /dev/null +++ b/app/Http/Requests/Auth/ChangePasswordRequest.php @@ -0,0 +1,27 @@ +check(); + } + + /** + * @return array|string> + */ + public function rules(): array + { + return [ + "current_password" => ["required", "string"], + "password" => ["required", "string", "min:8", "confirmed"], + ]; + } +} diff --git a/app/Http/Requests/Auth/ForgotPasswordRequest.php b/app/Http/Requests/Auth/ForgotPasswordRequest.php new file mode 100644 index 0000000..a9f9613 --- /dev/null +++ b/app/Http/Requests/Auth/ForgotPasswordRequest.php @@ -0,0 +1,26 @@ +|string> + */ + public function rules(): array + { + return [ + "email" => ["required", "string", "email", "max:255"], + ]; + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..2e0eab3 --- /dev/null +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,27 @@ +|string> + */ + public function rules(): array + { + return [ + "email" => ["required", "string", "email", "max:255"], + "password" => ["required", "string", "max:255"], + ]; + } +} diff --git a/app/Http/Requests/Auth/RegisterRequest.php b/app/Http/Requests/Auth/RegisterRequest.php new file mode 100644 index 0000000..a0bd06a --- /dev/null +++ b/app/Http/Requests/Auth/RegisterRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + "name" => ["required", "string", "max:255"], + "email" => ["required", "string", "email:rfc,dns", "max:255", "unique:users"], + "password" => ["required", "string", "min:8", "max:255", "confirmed"], + ]; + } +} diff --git a/app/Http/Requests/Auth/ResetPasswordRequest.php b/app/Http/Requests/Auth/ResetPasswordRequest.php new file mode 100644 index 0000000..31685fb --- /dev/null +++ b/app/Http/Requests/Auth/ResetPasswordRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + "token" => ["required", "string"], + "email" => ["required", "string", "email"], + "password" => ["required", "string", "min:8", "max:255", "confirmed"], + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 3f2c8ff..03125e6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -11,6 +11,7 @@ use Laravel\Sanctum\HasApiTokens; /** + * @property int $id * @property string $name * @property string $email * @property string $password diff --git a/app/Notifications/ForgotPasswordNotification.php b/app/Notifications/ForgotPasswordNotification.php new file mode 100644 index 0000000..735d503 --- /dev/null +++ b/app/Notifications/ForgotPasswordNotification.php @@ -0,0 +1,44 @@ + + */ + public function via(object $notifiable): array + { + return ["mail"]; + } + + public function toMail(object $notifiable): MailMessage + { + return new MailMessage() + ->subject("(MRRGroup) MiniStrava - Password Reset Code") + ->view("emails.forgotPassword", [ + "code" => $this->code, + "user" => $notifiable, + ]); + } + + /** + * @return array + */ + public function toArray(object $notifiable): array + { + return []; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 1e1695a..ec535e9 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -9,6 +9,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__ . "/../routes/web.php", + api: __DIR__ . "/../routes/api.php", commands: __DIR__ . "/../routes/console.php", health: "/up", ) diff --git a/resources/views/emails/forgotPassword.blade.php b/resources/views/emails/forgotPassword.blade.php new file mode 100644 index 0000000..7137f09 --- /dev/null +++ b/resources/views/emails/forgotPassword.blade.php @@ -0,0 +1,28 @@ + + + + + Password Reset Code + + +
+

Hello {{ $user->name ?? 'there' }}

+

+ We received a request to reset your account password. +

+

+ Your 6-digit reset code is: +

+
+ {{ $code }} +
+

+ This code will expire in 60 minutes. If you didn't request a password reset, feel free to ignore this message. +

+
+

+ © {{ date('Y') }} MRR Group. All rights reserved. +

+
+ + diff --git a/routes/api.php b/routes/api.php index 9a2e8ec..55af1e1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -5,5 +5,20 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; +use Strava\Http\Controllers\Auth\LoginController; +use Strava\Http\Controllers\Auth\LogoutController; +use Strava\Http\Controllers\Auth\PasswordController; +use Strava\Http\Controllers\Auth\RegisterController; Route::middleware("auth:sanctum")->get("/user", fn(Request $request): JsonResponse => new JsonResponse($request->user())); + +Route::middleware(["auth:sanctum"])->group(function (): void { + Route::post("/auth/logout", [LogoutController::class, "logout"])->name("logout"); + + Route::post("/user/change-password", [PasswordController::class, "changePassword"])->name("change-password"); +}); + +Route::post("/auth/login", [LoginController::class, "login"])->name("login"); +Route::post("/auth/register", [RegisterController::class, "register"])->name("register"); +Route::post("/auth/forgot-password", [PasswordController::class, "sendResetEmail"])->name("forgot-password"); +Route::post("/auth/reset-password", [PasswordController::class, "resetPassword"])->name("reset-password");