Pārlūkot izejas kodu

feat: :sparkles: feat (login) criação da pagina de login

foi criada a página de login do serprati com diferenciação por tipo de usuário e layout adequado

fase:dev | origin:escopo
Gustavo Zanatta 2 nedēļas atpakaļ
vecāks
revīzija
050062d7ae

+ 3 - 3
app/Enums/UserTypeEnum.php

@@ -8,7 +8,7 @@ enum UserTypeEnum: string
 {
     use EnumHelper;
 
-    case ADMIN = 'ADMIN';
-    case USER = 'USER';
-    case GUEST = 'GUEST';
+    case ADMINISTRADOR = 'administrador';
+    case ASSOCIADO = 'associado';
+    case PARCEIRO = 'parceiro';
 }

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

@@ -20,12 +20,17 @@ class AuthController extends Controller
         $result = $this->authService->login(
             email: $validated["email"],
             password: $validated["password"],
+            tipo: $validated["tipo"],
         );
 
         if (!$result) {
             return $this->errorResponse(message: __("auth.failed"), code: 401);
         }
 
+        if (isset($result["error"]) && $result["error"] === "wrong_type") {
+            return $this->errorResponse(message: __("auth.wrong_type"), code: 403);
+        }
+
         $cookieName = $this->getCookieName($request);
 
         return $this->successResponse(

+ 78 - 0
app/Http/Controllers/PasswordResetController.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\ForgotPasswordRequest;
+use App\Http\Requests\VerifyCodeRequest;
+use App\Http\Requests\ResetPasswordRequest;
+use App\Services\PasswordResetService;
+use Illuminate\Http\JsonResponse;
+
+class PasswordResetController extends Controller
+{
+    public function __construct(protected PasswordResetService $passwordResetService) {}
+
+    public function forgotPassword(ForgotPasswordRequest $request): JsonResponse
+    {
+        $validated = $request->validated();
+
+        $sent = $this->passwordResetService->sendCode(
+            email: $validated['email'],
+            tipo: $validated['tipo'],
+        );
+
+        if (!$sent) {
+            return $this->errorResponse(
+                message: __('auth.wrong_type'),
+                code: 403,
+            );
+        }
+
+        return $this->successResponse(
+            message: __('auth.password_reset_sent'),
+        );
+    }
+
+    public function verifyCode(VerifyCodeRequest $request): JsonResponse
+    {
+        $validated = $request->validated();
+
+        $valid = $this->passwordResetService->verifyCode(
+            email: $validated['email'],
+            code: $validated['codigo'],
+        );
+
+        if (!$valid) {
+            return $this->errorResponse(
+                message: __('auth.password_reset_invalid'),
+                code: 422,
+            );
+        }
+
+        return $this->successResponse(
+            message: 'OK',
+        );
+    }
+
+    public function resetPassword(ResetPasswordRequest $request): JsonResponse
+    {
+        $validated = $request->validated();
+
+        $reset = $this->passwordResetService->resetPassword(
+            email: $validated['email'],
+            code: $validated['codigo'],
+            password: $validated['password'],
+        );
+
+        if (!$reset) {
+            return $this->errorResponse(
+                message: __('auth.password_reset_invalid'),
+                code: 422,
+            );
+        }
+
+        return $this->successResponse(
+            message: __('auth.password_reset_success'),
+        );
+    }
+}

+ 1 - 0
app/Http/Requests/AuthRequest.php

@@ -11,6 +11,7 @@ class AuthRequest extends FormRequest
         return [
             "email" => "required|string|email",
             "password" => "required|string",
+            "tipo" => "required|string|in:administrador,associado,parceiro",
         ];
     }
 }

+ 16 - 0
app/Http/Requests/ForgotPasswordRequest.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ForgotPasswordRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        return [
+            "email" => "required|string|email|exists:users,email",
+            "tipo"  => "required|string|in:administrador,associado,parceiro",
+        ];
+    }
+}

+ 18 - 0
app/Http/Requests/ResetPasswordRequest.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ResetPasswordRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        return [
+            "email"                 => "required|string|email|exists:users,email",
+            "codigo"                => "required|string|size:6",
+            "password"              => "required|string|min:6|confirmed",
+            "password_confirmation" => "required|string",
+        ];
+    }
+}

+ 16 - 0
app/Http/Requests/VerifyCodeRequest.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class VerifyCodeRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        return [
+            "email"  => "required|string|email|exists:users,email",
+            "codigo" => "required|string|size:6",
+        ];
+    }
+}

+ 33 - 0
app/Mail/PasswordResetCode.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Mail;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Mail\Mailable;
+use Illuminate\Mail\Mailables\Content;
+use Illuminate\Mail\Mailables\Envelope;
+use Illuminate\Queue\SerializesModels;
+
+class PasswordResetCode extends Mailable
+{
+    use Queueable, SerializesModels;
+
+    public function __construct(
+        public readonly string $code,
+        public readonly string $userName,
+    ) {}
+
+    public function envelope(): Envelope
+    {
+        return new Envelope(
+            subject: 'Código de Verificação — SerpRati',
+        );
+    }
+
+    public function content(): Content
+    {
+        return new Content(
+            view: 'emails.password-reset-code',
+        );
+    }
+}

+ 12 - 2
app/Models/User.php

@@ -71,9 +71,19 @@ class User extends Authenticatable
         ];
     }
 
-    public function isAdmin(): bool
+    public function isAdministrador(): bool
     {
-        return $this->type === UserTypeEnum::ADMIN;
+        return $this->type === UserTypeEnum::ADMINISTRADOR;
+    }
+
+    public function isAssociado(): bool
+    {
+        return $this->type === UserTypeEnum::ASSOCIADO;
+    }
+
+    public function isParceiro(): bool
+    {
+        return $this->type === UserTypeEnum::PARCEIRO;
     }
 
     /**

+ 7 - 1
app/Services/AuthService.php

@@ -11,13 +11,19 @@ use Illuminate\Support\Str;
 
 class AuthService
 {
-    public function login(string $email, string $password): ?array
+    public function login(string $email, string $password, string $tipo): ?array
     {
         if (!Auth::attempt(["email" => $email, "password" => $password])) {
             return null;
         }
 
         $user = User::where("email", $email)->first();
+
+        if ($user->type->value !== $tipo) {
+            Auth::logout();
+            return ["error" => "wrong_type"];
+        }
+
         $deviceId = Str::uuid()->toString();
 
         $accessToken = $user->createAccessToken($deviceId);

+ 71 - 0
app/Services/PasswordResetService.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Services;
+
+use App\Mail\PasswordResetCode;
+use App\Models\User;
+use App\Enums\UserTypeEnum;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Mail;
+use Carbon\Carbon;
+
+class PasswordResetService
+{
+    private const CODE_TTL_MINUTES = 15;
+
+    public function sendCode(string $email, string $tipo): bool
+    {
+        $user = User::where('email', $email)->first();
+
+        if (!$user || $user->type->value !== $tipo) {
+            return false;
+        }
+
+        $code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
+
+        DB::table('password_reset_tokens')->updateOrInsert(
+            ['email' => $email],
+            [
+                'token'      => Hash::make($code),
+                'created_at' => Carbon::now(),
+            ]
+        );
+
+        Mail::to($email)->send(new PasswordResetCode($code, $user->name));
+
+        return true;
+    }
+
+    public function verifyCode(string $email, string $code): bool
+    {
+        $record = DB::table('password_reset_tokens')
+            ->where('email', $email)
+            ->first();
+
+        if (!$record) {
+            return false;
+        }
+
+        if (Carbon::parse($record->created_at)->addMinutes(self::CODE_TTL_MINUTES)->isPast()) {
+            return false;
+        }
+
+        return Hash::check($code, $record->token);
+    }
+
+    public function resetPassword(string $email, string $code, string $password): bool
+    {
+        if (!$this->verifyCode($email, $code)) {
+            return false;
+        }
+
+        User::where('email', $email)->update([
+            'password' => Hash::make($password),
+        ]);
+
+        DB::table('password_reset_tokens')->where('email', $email)->delete();
+
+        return true;
+    }
+}

+ 1 - 1
database/migrations/0001_01_01_000000_create_users_table.php

@@ -17,7 +17,7 @@ return new class extends Migration
             $table->string('email')->unique();
             $table->timestamp('email_verified_at')->nullable();
             $table->string('password');
-            $table->string('type')->default('GUEST');
+            $table->string('type')->default('associado');
             $table->string('language')->default('pt');
             $table->timestamps();
         });

+ 56 - 6
database/seeders/PermissionSeeder.php

@@ -58,17 +58,67 @@ class PermissionSeeder extends Seeder
                     ],
                 ],
             ],
+            [
+                "scope" => "parceiro",
+                "description" => "Parceiros e Convênios",
+                "bits" => Permission::MENU | Permission::VIEW,
+                "children" => [
+                    [
+                        "scope" => "parceiro.convenio",
+                        "description" => "Gestão de Parceiros/Convênios",
+                        "bits" => Permission::CRUD,
+                        "children" => [],
+                    ],
+                    [
+                        "scope" => "parceiro.servico",
+                        "description" => "Serviços dos Parceiros",
+                        "bits" => Permission::CRUD,
+                        "children" => [],
+                    ],
+                ],
+            ],
+            [
+                "scope" => "loja",
+                "description" => "Loja",
+                "bits" => Permission::MENU | Permission::VIEW,
+                "children" => [
+                    [
+                        "scope" => "loja.item",
+                        "description" => "Itens da Loja",
+                        "bits" => Permission::CRUD,
+                        "children" => [],
+                    ],
+                    [
+                        "scope" => "loja.pedido",
+                        "description" => "Pedidos da Loja",
+                        "bits" => Permission::CRUD,
+                        "children" => [],
+                    ],
+                ],
+            ],
+            [
+                "scope" => "agendamento",
+                "description" => "Agendamentos",
+                "bits" => Permission::ALL_PERMS,
+                "children" => [],
+            ],
+            [
+                "scope" => "notificacao",
+                "description" => "Notificações",
+                "bits" => Permission::ALL_PERMS,
+                "children" => [],
+            ],
+            [
+                "scope" => "categoria",
+                "description" => "Categorias",
+                "bits" => Permission::CRUD,
+                "children" => [],
+            ],
         ];
 
         $this->createPermissionsAndChildren(permissions: $permissions);
     }
 
-    /**
-     * Recursively creates or updates permissions and handles nesting.
-     *
-     * @param array $permissions The array of permission data.
-     * @param Permission|null $parent The parent Permission object (for nested sets).
-     */
     private function createPermissionsAndChildren(
         array $permissions,
         ?Permission $parent = null,

+ 45 - 5
database/seeders/UserSeeder.php

@@ -13,12 +13,52 @@ class UserSeeder extends Seeder
      */
     public function run(): void
     {
-        $user = User::firstOrNew([
-            'name' => 'suporte',
+        $userAdm1 = User::firstOrNew([
             'email' => 'suporte@softpar.inf.br',
-            'password' => 'S@ft2080.',
-            'type' => UserTypeEnum::ADMIN,
         ]);
-        $user->save();
+        $userAdm1->name = 'suporte';
+        $userAdm1->password = 'S@ft2080.';
+        $userAdm1->type = UserTypeEnum::ADMINISTRADOR;
+        $userAdm1->save();
+
+        $userAdm2 = User::firstOrNew([
+            'email' => 'admin2@softpar.inf.br',
+        ]);
+        $userAdm2->name = 'admin2';
+        $userAdm2->password = 'S@ft2080.';
+        $userAdm2->type = UserTypeEnum::ADMINISTRADOR;
+        $userAdm2->save();
+
+        $userAssociado1 = User::firstOrNew([
+            'email' => 'associado1@softpar.inf.br',
+        ]);
+        $userAssociado1->name = 'associado1';
+        $userAssociado1->password = 'S@ft2080.';
+        $userAssociado1->type = UserTypeEnum::ASSOCIADO;
+        $userAssociado1->save();
+
+        $userAssociado2 = User::firstOrNew([
+            'email' => 'associado2@softpar.inf.br',
+        ]);
+        $userAssociado2->name = 'associado2';
+        $userAssociado2->password = 'S@ft2080.';
+        $userAssociado2->type = UserTypeEnum::ASSOCIADO;
+        $userAssociado2->save();
+
+        $userParceiro1 = User::firstOrNew([
+            'email' => 'parceiro1@softpar.inf.br',
+        ]);
+        $userParceiro1->name = 'parceiro1';
+        $userParceiro1->password = 'S@ft2080.';
+        $userParceiro1->type = UserTypeEnum::PARCEIRO;
+        $userParceiro1->save();
+
+        $userParceiro2 = User::firstOrNew([
+            'email' => 'parceiro2@softpar.inf.br',
+        ]);
+        $userParceiro2->name = 'parceiro2';
+        $userParceiro2->password = 'S@ft2080.';
+        $userParceiro2->type = UserTypeEnum::PARCEIRO;
+        $userParceiro2->save();
     }
 }

+ 17 - 9
database/seeders/UserTypePermissionSeeder.php

@@ -15,7 +15,7 @@ class UserTypePermissionSeeder extends Seeder
         foreach (UserTypeEnum::cases() as $userType) {
             $dataToSync = [];
             switch ($userType) {
-                case UserTypeEnum::ADMIN:
+                case UserTypeEnum::ADMINISTRADOR:
                     foreach ($allPermissions as $scope => $perm) {
                         $dataToSync[] = [
                             'scope' => $scope,
@@ -23,18 +23,26 @@ class UserTypePermissionSeeder extends Seeder
                         ];
                     }
                     break;
-                case UserTypeEnum::USER:
+
+                case UserTypeEnum::ASSOCIADO:
                     $dataToSync = [
-                        ['scope' => 'dashboard',      'bits' => Permission::VIEW],
-                        ['scope' => 'config.user',    'bits' => Permission::VIEW | Permission::EDIT],
-                        ['scope' => 'config.city',    'bits' => Permission::VIEW],
-                        ['scope' => 'config.country', 'bits' => Permission::VIEW],
-                        ['scope' => 'config.state',   'bits' => Permission::VIEW],
+                        ['scope' => 'dashboard',        'bits' => Permission::VIEW | Permission::MENU],
+                        ['scope' => 'parceiro',         'bits' => Permission::VIEW | Permission::MENU],
+                        ['scope' => 'parceiro.convenio','bits' => Permission::VIEW | Permission::MENU],
+                        ['scope' => 'loja',             'bits' => Permission::VIEW | Permission::MENU],
+                        ['scope' => 'loja.item',        'bits' => Permission::VIEW | Permission::MENU],
+                        ['scope' => 'loja.pedido',      'bits' => Permission::VIEW | Permission::ADD | Permission::MENU],
+                        ['scope' => 'agendamento',      'bits' => Permission::VIEW | Permission::ADD | Permission::MENU],
                     ];
                     break;
-                case UserTypeEnum::GUEST:
+
+                case UserTypeEnum::PARCEIRO:
                     $dataToSync = [
-                        ['scope' => 'config.user', 'bits' => Permission::VIEW],
+                        ['scope' => 'dashboard',        'bits' => Permission::VIEW | Permission::MENU],
+                        ['scope' => 'parceiro',         'bits' => Permission::VIEW | Permission::EDIT | Permission::MENU],
+                        ['scope' => 'parceiro.convenio','bits' => Permission::VIEW | Permission::EDIT | Permission::MENU],
+                        ['scope' => 'parceiro.servico', 'bits' => Permission::VIEW | Permission::ADD | Permission::EDIT | Permission::DELETE | Permission::MENU],
+                        ['scope' => 'agendamento',      'bits' => Permission::VIEW | Permission::MENU],
                     ];
                     break;
             }

+ 4 - 0
lang/en/auth.php

@@ -21,4 +21,8 @@ return [
     'already_logged_out' => 'User already logged out',
     'unauthorized' => 'Unauthorized',
     'session_expired' => 'Session expired',
+    'wrong_type' => 'User found, but the selected type is incorrect. Please select the correct login type and try again.',
+    'password_reset_sent' => 'Verification code sent to your email.',
+    'password_reset_invalid' => 'Invalid or expired code.',
+    'password_reset_success' => 'Password changed successfully.',
 ];

+ 4 - 0
lang/es/auth.php

@@ -21,4 +21,8 @@ return [
     'already_logged_out' => 'El usuario ya ha cerrado sesión',
     'unauthorized' => 'No autorizado',
     'session_expired' => 'Sesión caducada',
+    'wrong_type' => 'Usuario encontrado, pero el tipo seleccionado es incorrecto. Seleccione el tipo de inicio de sesión correcto e inténtelo de nuevo.',
+    'password_reset_sent' => 'Código de verificación enviado a su correo electrónico.',
+    'password_reset_invalid' => 'Código inválido o caducado.',
+    'password_reset_success' => 'Contraseña cambiada correctamente.',
 ];

+ 4 - 0
lang/pt/auth.php

@@ -21,4 +21,8 @@ return [
     'already_logged_out' => 'Usuário já desconectado',
     'unauthorized' => 'Não autorizado',
     'session_expired' => 'Sessão expirada',
+    'wrong_type' => 'Usuário encontrado, mas o tipo selecionado está incorreto. Selecione o login correto e tente novamente.',
+    'password_reset_sent' => 'Código de verificação enviado para seu e-mail.',
+    'password_reset_invalid' => 'Código inválido ou expirado.',
+    'password_reset_success' => 'Senha alterada com sucesso.',
 ];

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

@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="pt-BR">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <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); }
+        .header { background-color: #7B1FA2; padding: 32px 24px; text-align: center; }
+        .header h1 { color: #ffffff; margin: 0; font-size: 22px; font-weight: 700; letter-spacing: 1px; }
+        .body { padding: 32px 24px; }
+        .body p { color: #444; font-size: 15px; line-height: 1.6; margin: 0 0 16px; }
+        .code-box { background-color: #F3E5F5; border: 2px dashed #7B1FA2; border-radius: 8px; padding: 20px; text-align: center; margin: 24px 0; }
+        .code-box span { font-size: 36px; font-weight: 700; color: #7B1FA2; letter-spacing: 8px; }
+        .footer { padding: 16px 24px; background-color: #f9f9f9; text-align: center; }
+        .footer p { color: #999; font-size: 12px; margin: 0; }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="header">
+            <h1>SerpRati</h1>
+        </div>
+        <div class="body">
+            <p>Olá, <strong>{{ $userName }}</strong>!</p>
+            <p>Recebemos uma solicitação para redefinir a senha da sua conta. Use o código abaixo para continuar:</p>
+            <div class="code-box">
+                <span>{{ $code }}</span>
+            </div>
+            <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>
+        </div>
+    </div>
+</body>
+</html>

+ 8 - 0
routes/noAuthRoutes/password_reset.php

@@ -0,0 +1,8 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\PasswordResetController;
+
+Route::post('/forgot-password', [PasswordResetController::class, 'forgotPassword']);
+Route::post('/verify-code', [PasswordResetController::class, 'verifyCode']);
+Route::post('/reset-password', [PasswordResetController::class, 'resetPassword']);