Jelajahi Sumber

✨ feat(auth): implementar fluxo de autenticação e recuperação de senha via código

Fase: dev | Origin: melhoria-interna
Gustavo Zanatta 1 Minggu lalu
induk
melakukan
9ddb24e0a9

+ 0 - 8
app/Http/Controllers/AuthController.php

@@ -101,12 +101,4 @@ class AuthController extends Controller
         );
     }
 
-    /**
-     * Resolves the dynamic cookie name based on the requesting application.
-     */
-    private function getCookieName(mixed $request): string
-    {
-        $appOrigin = $request->header("X-App-Origin", "default");
-        return "{$appOrigin}_refresh_token";
-    }
 }

+ 31 - 3
app/Http/Controllers/PasswordResetController.php

@@ -5,12 +5,19 @@ namespace App\Http\Controllers;
 use App\Http\Requests\ForgotPasswordRequest;
 use App\Http\Requests\VerifyCodeRequest;
 use App\Http\Requests\ResetPasswordRequest;
+use App\Http\Resources\UserResource;
+use App\Services\AuthService;
 use App\Services\PasswordResetService;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
 
 class PasswordResetController extends Controller
 {
-    public function __construct(protected PasswordResetService $passwordResetService) {}
+    public function __construct(
+        protected PasswordResetService $passwordResetService,
+        protected AuthService $authService,
+    ) {}
 
     public function forgotPassword(ForgotPasswordRequest $request): JsonResponse
     {
@@ -58,21 +65,42 @@ class PasswordResetController extends Controller
     {
         $validated = $request->validated();
 
-        $reset = $this->passwordResetService->resetPassword(
+        $user = $this->passwordResetService->resetPassword(
             email: $validated['email'],
             code: $validated['codigo'],
             password: $validated['password'],
         );
 
-        if (!$reset) {
+        if (!$user) {
             return $this->errorResponse(
                 message: __('auth.password_reset_invalid'),
                 code: 422,
             );
         }
 
+        $deviceId     = Str::uuid()->toString();
+        $accessToken  = $user->createAccessToken($deviceId);
+        $refreshToken = $user->createRefreshToken($deviceId);
+        $cookieName   = $this->getCookieName($request);
+
         return $this->successResponse(
+            payload: [
+                'access_token' => $accessToken,
+                'user'         => new UserResource($user),
+            ],
             message: __('auth.password_reset_success'),
+        )->withCookie(
+            cookie(
+                $cookieName,
+                $refreshToken,
+                config('sanctum.rt_expiration') * 60,
+                '/',
+                config('session.domain'),
+                config('session.secure'),
+                true,
+                false,
+                'Lax',
+            ),
         );
     }
 }

+ 25 - 10
app/Http/Requests/UserRequest.php

@@ -4,8 +4,9 @@ namespace App\Http\Requests;
 
 use Illuminate\Foundation\Http\FormRequest;
 use App\Enums\{
-    UserTypeEnum,
-    LanguageEnum
+    LanguageEnum,
+    UserStatusEnum,
+    UserTypeEnum
 };
 use Illuminate\Validation\Rule;
 
@@ -13,18 +14,32 @@ class UserRequest extends FormRequest
 {
     public function rules(): array
     {
+        $emailUnique = 'unique:users,email';
+        $cpfUnique   = 'unique:users,cpf';
+        if ($this->isMethod('put') && $this->route('id')) {
+            $emailUnique = 'unique:users,email,' . $this->route('id');
+            $cpfUnique   = 'unique:users,cpf,' . $this->route('id');
+        }
+
         $rules = [
-            'avatar' => 'sometimes|string|nullable',
-            'name' => 'sometimes|string|nullable',
-            'email' => 'sometimes|email|unique:users,email',
-            'password' => 'sometimes|string|nullable',
-            'type' => ['sometimes', Rule::enum(UserTypeEnum::class)],
-            'language' => ['sometimes', Rule::enum(LanguageEnum::class)],
+            'avatar'         => 'sometimes|string|nullable',
+            'name'           => 'sometimes|string|nullable',
+            'email'          => ['sometimes', 'email', $emailUnique],
+            'password'       => 'sometimes|string|nullable',
+            'type'           => ['sometimes', Rule::enum(UserTypeEnum::class)],
+            'language'       => ['sometimes', Rule::enum(LanguageEnum::class)],
+            'cpf'            => ['sometimes', 'nullable', 'string', 'max:14', $cpfUnique],
+            'registration'   => 'sometimes|nullable|string|max:50',
+            'status'         => ['sometimes', Rule::enum(UserStatusEnum::class)],
+            'admission_date' => 'sometimes|nullable|date',
+            'expiry_date'    => 'sometimes|nullable|date',
+            'position_id'    => 'sometimes|nullable|integer|exists:positions,id',
+            'sector_id'      => 'sometimes|nullable|integer|exists:sectors,id',
         ];
 
         if ($this->isMethod('post')) {
-            $rules['name'] = 'required|string|max:255';
-            $rules['email'] = 'required|email|unique:users,email';
+            $rules['name']     = 'required|string|max:255';
+            $rules['email']    = ['required', 'email', $emailUnique];
             $rules['password'] = 'required|string|min:6';
             if (!$this->has('language')) {
                 $this->merge(['language' => LanguageEnum::PORTUGUESE->value]);

+ 1 - 1
app/Mail/PasswordResetCode.php

@@ -20,7 +20,7 @@ class PasswordResetCode extends Mailable
     public function envelope(): Envelope
     {
         return new Envelope(
-            subject: 'Código de Verificação — SerpRati',
+            subject: 'Código de Verificação — SerPrati',
         );
     }
 

+ 10 - 6
app/Services/PasswordResetService.php

@@ -54,18 +54,22 @@ class PasswordResetService
         return Hash::check($code, $record->token);
     }
 
-    public function resetPassword(string $email, string $code, string $password): bool
+    public function resetPassword(string $email, string $code, string $password): ?User
     {
         if (!$this->verifyCode($email, $code)) {
-            return false;
+            return null;
+        }
+
+        $user = User::where('email', $email)->first();
+
+        if (!$user) {
+            return null;
         }
 
-        User::where('email', $email)->update([
-            'password' => Hash::make($password),
-        ]);
+        $user->update(['password' => Hash::make($password)]);
 
         DB::table('password_reset_tokens')->where('email', $email)->delete();
 
-        return true;
+        return $user;
     }
 }

+ 3 - 3
resources/views/emails/password-reset-code.blade.php

@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Código de Verificação — SerpRati</title>
+    <title>Código de Verificação — SerPrati</title>
     <style>
         body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
         .container { max-width: 480px; margin: 40px auto; background: #ffffff; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
@@ -20,7 +20,7 @@
 <body>
     <div class="container">
         <div class="header">
-            <h1>SerpRati</h1>
+            <h1>SerPrati</h1>
         </div>
         <div class="body">
             <p>Olá, <strong>{{ $userName }}</strong>!</p>
@@ -31,7 +31,7 @@
             <p>Este código é válido por <strong>15 minutos</strong>. Se você não solicitou a redefinição de senha, ignore este e-mail — sua conta permanece segura.</p>
         </div>
         <div class="footer">
-            <p>© {{ date('Y') }} SerpRati. Todos os direitos reservados.</p>
+            <p>© {{ date('Y') }} SerPrati. Todos os direitos reservados.</p>
         </div>
     </div>
 </body>