Explorar o código

feat: :sparkles: feat (login) bloqueio de logins + provider aprovacao

foi realizado o bloqueio de logins nos diferentes sistemas, permitindo apenas clients logarem no app do cliente, apenas providers logarem no app do cliente e apenas users tipo admin e user logar no backoffice + provider precisa ser aprovado

fase:dev | origin:escopo
Gustavo Zanatta hai 1 mes
pai
achega
ece2bcd485

+ 14 - 0
app/Enums/ApprovalStatusEnum.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Enums;
+
+use App\Traits\EnumHelper;
+
+enum ApprovalStatusEnum: string
+{
+    use EnumHelper;
+
+    case PENDING  = 'pending';
+    case ACCEPTED = 'accepted';
+    case REJECTED = 'rejected';
+}

+ 83 - 3
app/Http/Controllers/AuthController.php

@@ -141,13 +141,93 @@ class AuthController extends Controller
     );
   }
 
-  public function sendCode(UserAppsRequest $request): JsonResponse
+  public function clientSendCode(UserAppsRequest $request): JsonResponse
   {
-    $isLogin = $this->authService->sendCode($request->validated());
+    $result = $this->authService->clientSendCode($request->validated());
+
+    if (is_array($result) && isset($result['error'])) {
+      return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+    }
+
     return $this->successResponse(
       message: __("messages.code_sent"),
       code: 201,
-      payload: ['isLogin' => $isLogin],
+      payload: ['isLogin' => $result],
+    );
+  }
+
+  public function providerSendCode(UserAppsRequest $request): JsonResponse
+  {
+    $result = $this->authService->providerSendCode($request->validated());
+
+    if (is_array($result) && isset($result['error'])) {
+      return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+    }
+
+    return $this->successResponse(
+      message: __("messages.code_sent"),
+      code: 201,
+      payload: ['isLogin' => $result],
+    );
+  }
+
+  public function validateCodeClient(UserAppsValidateCodeRequest $request): JsonResponse
+  {
+    $email   = $request->input('email');
+    $phone   = $request->input('phone');
+    $code    = $request->input('code');
+    $isLogin = (bool) $request->input('isLogin', false);
+
+    $result = $this->authService->validateCodeClient($request->validated(), $isLogin);
+
+    if ($result === false) {
+      return $this->errorResponse(message: __('auth.invalid_code'), code: 400);
+    }
+
+    if (is_array($result) && isset($result['error'])) {
+      return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+    }
+
+    if ($isLogin) {
+      return $this->successResponse(
+        payload: new AuthResource([...$result['payload'], 'refresh_token' => $result['refreshToken']]),
+        message: __('auth.logged_in'),
+      );
+    }
+
+    return $this->successResponse(
+      payload: ['email' => $email, 'phone' => $phone, 'code' => $code],
+      message: __('auth.valid_code'),
+    );
+  }
+
+  public function validateCodeProvider(UserAppsValidateCodeRequest $request): JsonResponse
+  {
+    $email   = $request->input('email');
+    $phone   = $request->input('phone');
+    $code    = $request->input('code');
+    $isLogin = (bool) $request->input('isLogin', false);
+
+    $result = $this->authService->validateCodeProvider($request->validated(), $isLogin);
+
+    if ($result === false) {
+      return $this->errorResponse(message: __('auth.invalid_code'), code: 400);
+    }
+
+    if (is_array($result) && isset($result['error'])) {
+      return $this->errorResponse(message: __("auth.{$result['error']}"), code: 403);
+    }
+
+    if ($isLogin) {
+      return $this->successResponse(
+        payload: new AuthResource([...$result['payload'], 'refresh_token' => $result['refreshToken']]),
+        message: __('auth.logged_in'),
+      );
+    }
+
+    return $this->successResponse(
+      payload: ['email' => $email, 'phone' => $phone, 'code' => $code],
+      message: __('auth.valid_code'),
     );
   }
 

+ 33 - 0
app/Http/Controllers/ProviderController.php

@@ -7,6 +7,7 @@ use App\Http\Requests\ProviderRequest;
 use App\Http\Requests\RegisterProviderRequest;
 use App\Http\Resources\ProviderResource;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
 use App\Http\Resources\AuthResource;
 
 class ProviderController extends Controller
@@ -55,6 +56,38 @@ class ProviderController extends Controller
         );
     }
 
+    public function pending(Request $request): JsonResponse
+    {
+        $page    = $request->integer('page', 1);
+        $perPage = $request->integer('per_page', 10);
+        $paginated = $this->service->getPending($page, $perPage);
+
+        return $this->successResponse(payload: [
+            'data'  => ProviderResource::collection($paginated->items()),
+            'total' => $paginated->total(),
+            'from'  => $paginated->firstItem() ?? 0,
+            'to'    => $paginated->lastItem() ?? 0,
+        ]);
+    }
+
+    public function approve(int $id): JsonResponse
+    {
+        $item = $this->service->approve($id);
+        return $this->successResponse(
+            payload: new ProviderResource($item),
+            message: __('messages.provider_approved'),
+        );
+    }
+
+    public function reject(int $id): JsonResponse
+    {
+        $item = $this->service->reject($id);
+        return $this->successResponse(
+            payload: new ProviderResource($item),
+            message: __('messages.provider_rejected'),
+        );
+    }
+
     public function register(RegisterProviderRequest $request): JsonResponse
     {
       $result = $this->service->register($request->validated());

+ 2 - 1
app/Http/Requests/ProviderRequest.php

@@ -5,6 +5,7 @@ namespace App\Http\Requests;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Validation\Rule;
 use App\Enums\UserTypeEnum;
+use App\Enums\ApprovalStatusEnum;
 
 class ProviderRequest extends FormRequest
 {
@@ -60,7 +61,7 @@ class ProviderRequest extends FormRequest
             'birth_date' => 'sometimes|nullable|date|before:today',
             'selfie_verified' => 'sometimes|boolean',
             'document_verified' => 'sometimes|boolean',
-            'is_approved' => 'sometimes|boolean',
+            'approval_status' => ['sometimes', Rule::enum(ApprovalStatusEnum::class)],
             'daily_price_8h' => 'sometimes|nullable|numeric|min:100|max:500',
             'daily_price_6h' => 'sometimes|nullable|numeric',
             'daily_price_4h' => 'sometimes|nullable|numeric',

+ 0 - 2
app/Http/Requests/RegisterProviderRequest.php

@@ -56,8 +56,6 @@ class RegisterProviderRequest extends FormRequest
       'selfie_base64' => 'required|string',
       'document_front_base64' => 'required|string',
       'document_back_base64' => 'required|string',
-
-      'is_approved' => 'sometimes|boolean',
     ];
 
     if (!$this->has('email')) {

+ 0 - 2
app/Http/Requests/UserAppsRequest.php

@@ -16,8 +16,6 @@ class UserAppsRequest extends FormRequest
     $rules = [
       'email' => 'sometimes|email',
       'phone' => 'sometimes|string|nullable',
-      'type' => ['sometimes', Rule::enum(UserTypeEnum::class)],
-      'code' => 'sometimes|string|nullable',
     ];
 
     if (!$this->has('email')) {

+ 3 - 3
app/Http/Resources/ProviderResource.php

@@ -23,15 +23,15 @@ class ProviderResource extends JsonResource
             'birth_date' => $this->birth_date ? Carbon::parse($this->birth_date)->format('d/m/Y') : null,
             'selfie_verified' => $this->selfie_verified,
             'document_verified' => $this->document_verified,
-            'is_approved' => $this->is_approved,
+            'approval_status' => $this->approval_status?->value ?? $this->approval_status,
             'daily_price_8h' => $this->daily_price_8h,
             'daily_price_6h' => $this->daily_price_6h,
             'daily_price_4h' => $this->daily_price_4h,
             'daily_price_2h' => $this->daily_price_2h,
             'profile_media_id' => $this->profile_media_id,
             'profile_media' => $this->profileMedia,
-            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i'),
-            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i'),
+            'created_at' => Carbon::parse($this->created_at)->format('d/m/Y H:i'),
+            'updated_at' => Carbon::parse($this->updated_at)->format('d/m/Y H:i'),
         ];
     }
 

+ 4 - 3
app/Models/Provider.php

@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use App\Enums\ApprovalStatusEnum;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -19,7 +20,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property string|null $birth_date
  * @property bool $selfie_verified
  * @property bool $document_verified
- * @property bool $is_approved
+ * @property string $approval_status
  * @property float|null $daily_price_8h
  * @property float|null $daily_price_6h
  * @property float|null $daily_price_4h
@@ -55,9 +56,9 @@ class Provider extends Model
   {
     return [
       "birth_date" => "date",
-      "selfie_verified" => "boolean",
+      "selfie_verified"   => "boolean",
       "document_verified" => "boolean",
-      "is_approved" => "boolean",
+      "approval_status"   => ApprovalStatusEnum::class,
       "average_rating" => "decimal:1",
       "daily_price_8h" => "decimal:2",
       "daily_price_6h" => "decimal:2",

+ 143 - 8
app/Services/AuthService.php

@@ -4,6 +4,9 @@ namespace App\Services;
 
 use App\Models\User;
 use App\Models\PersonalAccessToken;
+use App\Enums\UserTypeEnum;
+use App\Enums\ApprovalStatusEnum;
+use App\Models\Provider;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
@@ -18,22 +21,28 @@ class AuthService
 
   public function login(string $email, string $password): ?array
   {
-    if (!Auth::attempt(["email" => $email, "password" => $password]) || (User::where("email", $email)->first()->type == 'CLIENT' || User::where("email", $email)->first()->type == 'PROVIDER')) {
+    $user = User::where('email', $email)->first();
+
+    if (!$user || !in_array($user->type, [UserTypeEnum::ADMIN, UserTypeEnum::USER])) {
+      return null;
+    }
+
+    if (!Auth::attempt(['email' => $email, 'password' => $password])) {
       return null;
     }
 
-    $user = User::where("email", $email)->first();
+    // $user = User::where('email', $email)->first();
     $deviceId = Str::uuid()->toString();
 
     $accessToken = $user->createAccessToken($deviceId);
     $refreshToken = $user->createRefreshToken($deviceId);
 
     return [
-      "payload" => [
-        "access_token" => $accessToken,
-        "user" => $user,
+      'payload' => [
+        'access_token' => $accessToken,
+        'user'         => $user,
       ],
-      "refreshToken" => $refreshToken,
+      'refreshToken' => $refreshToken,
     ];
   }
 
@@ -109,7 +118,7 @@ class AuthService
     });
   }
 
-  public function sendCode(array $data): ?bool
+  public function clientSendCode(array $data): bool|array|null
   {
     try {
       DB::beginTransaction();
@@ -124,9 +133,76 @@ class AuthService
           });
       })
         ->first();
+      $isLogin = false;
+      if ($user) {
+        if ($user->type->value !== UserTypeEnum::CLIENT->value) {
+          DB::rollBack();
+          return ['error' => 'wrong_user_type'];
+        }
+        $user->code = $code;
+        $user->validated_code = false;
+        $user->save();
+        $isLogin = true;
+      } else {
+        $user = new User();
+        $user->fill($data);
+        $user->code = $code;
+        $user->name = $data['name'] ?? 'Usuário';
+        $user->type = $data['type'] ?? UserTypeEnum::CLIENT->value;
+        $user->save();
+      }
+
+      if (!empty($data['email'])) {
+        $this->emailService->sendVerificationCode(
+          email: $data['email'],
+          code: $code,
+          recipientName: $data['name'] ?? '',
+        );
+      } elseif (!empty($data['phone'])) {
+        Log::info('SMS: envio de código por telefone ainda não implementado.', [
+          'phone' => $data['phone'],
+        ]);
+      }
+
+      DB::commit();
+      return $isLogin;
+    } catch (\Exception $e) {
+      DB::rollBack();
+      Log::error('Erro ao enviar código de verificação.', [
+        'error' => $e->getMessage(),
+        'data' => $data,
+      ]);
+      return false;
+    }
+  }
+
+  public function providerSendCode(array $data): bool|array|null
+  {
+    try {
+      DB::beginTransaction();
+      $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
 
+      $user = User::where(function ($query) use ($data) {
+        $query->when(!empty($data['email']), function ($q) use ($data) {
+          $q->where('email', $data['email']);
+        })
+          ->when(!empty($data['phone']), function ($q) use ($data) {
+            $q->where('phone', $data['phone']);
+          });
+      })
+        ->first();
       $isLogin = false;
       if ($user) {
+        if ($user->type->value !== UserTypeEnum::PROVIDER->value) {
+          DB::rollBack();
+          return ['error' => 'wrong_user_type'];
+        }
+        $provider = Provider::where('user_id', $user->id)->first();
+        if($provider && $provider->approval_status->value !== ApprovalStatusEnum::ACCEPTED->value) {
+          DB::rollBack();
+          return ['error' => 'provider_not_accepted'];
+        }
+         
         $user->code = $code;
         $user->validated_code = false;
         $user->save();
@@ -136,7 +212,7 @@ class AuthService
         $user->fill($data);
         $user->code = $code;
         $user->name = $data['name'] ?? 'Usuário';
-        $user->type = $data['type'] ?? 'USER';
+        $user->type = $data['type'] ?? UserTypeEnum::PROVIDER->value;
         $user->save();
       }
 
@@ -164,6 +240,65 @@ class AuthService
     }
   }
 
+  public function validateCodeClient(array $data, bool $isLogin): bool|array
+  {
+    $email = $data['email'] ?? null;
+    $phone = $data['phone'] ?? null;
+    $code  = $data['code'] ?? '';
+
+    $user = User::where(function ($query) use ($email, $phone) {
+        $query->when($email, fn($q) => $q->where('email', $email))
+              ->when($phone,  fn($q) => $q->where('phone', $phone));
+      })
+      ->where('code', $code)
+      ->first();
+
+    if (!$user) {
+      return false;
+    }
+
+    if ($isLogin) {
+      return $this->loginWithEmail($user->email, $code);
+    }
+
+    return true;
+  }
+
+  public function validateCodeProvider(array $data, bool $isLogin): bool|array
+  {
+    $email = $data['email'] ?? null;
+    $phone = $data['phone'] ?? null;
+    $code  = $data['code'] ?? '';
+
+    $user = User::where(function ($query) use ($email, $phone) {
+        $query->when($email, fn($q) => $q->where('email', $email))
+              ->when($phone,  fn($q) => $q->where('phone', $phone));
+      })
+      ->where('code', $code)
+      ->first();
+
+    if (!$user) {
+      return false;
+    }
+
+    if ($isLogin) {
+      $user->load('provider');
+      $provider = $user->provider ?? null;
+
+      if ($provider && $provider->approval_status === ApprovalStatusEnum::PENDING->value) {
+        return ['error' => 'provider_pending'];
+      }
+
+      if ($provider && $provider->approval_status === ApprovalStatusEnum::REJECTED->value) {
+        return ['error' => 'provider_rejected'];
+      }
+
+      return $this->loginWithEmail($user->email, $code);
+    }
+
+    return true;
+  }
+
   public function validateCode(array $data, bool $isLogin): bool|array
   {
     $email = $data['email'] ?? null;

+ 30 - 1
app/Services/ProviderService.php

@@ -2,6 +2,7 @@
 
 namespace App\Services;
 
+use App\Enums\ApprovalStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Models\Address;
 use App\Models\City;
@@ -11,6 +12,7 @@ use App\Models\ProviderWorkingDay;
 use App\Models\State;
 use App\Models\User;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
@@ -63,6 +65,33 @@ class ProviderService
     return $model->delete();
   }
 
+  public function getPending(int $page = 1, int $perPage = 10): LengthAwarePaginator
+  {
+    return Provider::query()
+      ->where('approval_status', ApprovalStatusEnum::PENDING->value)
+      ->with(['user', 'profileMedia'])
+      ->orderBy('created_at', 'asc')
+      ->paginate($perPage, ['*'], 'page', $page);
+  }
+
+  public function approve(int $id): Provider
+  {
+    return DB::transaction(function () use ($id) {
+      $provider = Provider::findOrFail($id);
+      $provider->update(['approval_status' => ApprovalStatusEnum::ACCEPTED->value]);
+      return $provider->fresh(['user', 'profileMedia']);
+    });
+  }
+
+  public function reject(int $id): Provider
+  {
+    return DB::transaction(function () use ($id) {
+      $provider = Provider::findOrFail($id);
+      $provider->update(['approval_status' => ApprovalStatusEnum::REJECTED->value]);
+      return $provider->fresh(['user', 'profileMedia']);
+    });
+  }
+
   public function register(array $data): ?array
   {
     try {
@@ -112,7 +141,7 @@ class ProviderService
       $provider->daily_price_6h = $data['daily_price_6h'] ?? null;
       $provider->daily_price_4h = $data['daily_price_4h'] ?? null;
       $provider->daily_price_2h = $data['daily_price_2h'] ?? null;
-      $provider->is_approved = false;
+      $provider->approval_status = ApprovalStatusEnum::PENDING->value;
       $provider->selfie_media_base64 = $data['selfie_base64'] ?? null;
       $provider->document_front_media_base64 = $data['document_front_base64'] ?? null;
       $provider->document_back_media_base64 = $data['document_back_base64'] ?? null;

+ 30 - 0
database/migrations/2026_05_06_112533_replace_is_approved_with_approval_status_on_providers_table.php

@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->dropColumn('is_approved');
+            $table->string('approval_status')->default('pending')->after('document_verified');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->dropColumn('approval_status');
+            $table->boolean('is_approved')->default(false)->after('document_verified');
+        });
+    }
+};

+ 4 - 0
lang/en/auth.php

@@ -23,4 +23,8 @@ return [
     'session_expired' => 'Session expired',
     'invalid_code' => 'Invalid code',
     'valid_code' => 'The provided code is valid.',
+    'wrong_user_type' => 'This email cannot access this application. Please contact support for more information.',
+    // 'provider_pending' => 'Your registration is awaiting approval. We will contact you soon.',
+    // 'provider_rejected' => 'Your registration was rejected. Please contact support.',
+    'provider_not_accepted' => 'Your registration has not been approved yet. Please wait or contact support for more information.',
 ];

+ 2 - 0
lang/en/messages.php

@@ -12,4 +12,6 @@ return [
     'buyer_not_allowed' => 'Buyer not allowed',
     'code_sent' => 'Verification code sent successfully',
     'user_not_found_or_code_not_validated' => 'User not found or invalid code.',
+    'provider_approved' => 'Provider approved successfully.',
+    'provider_rejected' => 'Provider rejected.',
 ];

+ 4 - 0
lang/es/auth.php

@@ -23,4 +23,8 @@ return [
     'session_expired' => 'Sesión caducada',
     'invalid_code' => 'Código inválido',
     'valid_code' => 'El código proporcionado es válido.',
+    'wrong_user_type' => 'Este correo electrónico no puede acceder a esta aplicación. Contacte al soporte para más información.',
+    // 'provider_pending' => 'Su registro está pendiente de aprobación. Nos pondremos en contacto pronto.',
+    // 'provider_rejected' => 'Su registro fue rechazado. Póngase en contacto con el soporte.',
+    'provider_not_accepted' => 'Su registro aún no ha sido aprobado. Por favor, espere o póngase en contacto con el soporte para más información.',
 ];

+ 2 - 0
lang/es/messages.php

@@ -12,4 +12,6 @@ return [
     'buyer_not_allowed' => 'Comprador no permitido',
     'code_sent' => 'Código de verificación enviado exitosamente',
     'user_not_found_or_code_not_validated' => 'Usuario no encontrado o código inválido.',
+    'provider_approved' => 'Prestador aprobado exitosamente.',
+    'provider_rejected' => 'Prestador rechazado.',
 ];

+ 4 - 0
lang/pt/auth.php

@@ -23,4 +23,8 @@ return [
     'session_expired' => 'Sessão expirada',
     'invalid_code' => 'Código inválido',
     'valid_code' => 'O código fornecido é válido.',
+    'wrong_user_type' => 'Este e-mail não pode acessar esse aplicativo. Entre em contato com o suporte para mais informações.',
+    // 'provider_pending' => 'Seu cadastro está aguardando aprovação. Em breve entraremos em contato.',
+    // 'provider_rejected' => 'Seu cadastro foi recusado. Entre em contato com o suporte.',
+    'provider_not_accepted' => 'Seu cadastro ainda não foi aprovado. Por favor, aguarde ou entre em contato com o suporte para mais informações.',
 ];

+ 2 - 0
lang/pt/messages.php

@@ -12,4 +12,6 @@ return [
     'buyer_not_allowed' => 'Compra não permitida, tente outro ingresso ou comprador',
     'code_sent' => 'Código de verificação enviado com sucesso',
     'user_not_found_or_code_not_validated' => 'Usuário não encontrado ou código inválido.',
+    'provider_approved' => 'Prestador aprovado com sucesso.',
+    'provider_rejected' => 'Prestador recusado.',
 ];

+ 3 - 0
routes/authRoutes/provider.php

@@ -3,6 +3,9 @@
 use Illuminate\Support\Facades\Route;
 use App\Http\Controllers\ProviderController;
 
+Route::get('/provider/pending', [ProviderController::class, 'pending'])->middleware('permission:config.provider,view');
+Route::patch('/provider/{id}/approve', [ProviderController::class, 'approve'])->middleware('permission:config.provider,edit');
+Route::patch('/provider/{id}/reject', [ProviderController::class, 'reject'])->middleware('permission:config.provider,edit');
 Route::get('/provider', [ProviderController::class, 'index'])->middleware('permission:config.provider,view');
 Route::post('/provider', [ProviderController::class, 'store'])->middleware('permission:config.provider,add');
 Route::get('/provider/{id}', [ProviderController::class, 'show'])->middleware('permission:config.provider,view');

+ 4 - 1
routes/noAuthRoutes/auth.php

@@ -12,7 +12,10 @@ Route::post('/refresh', [AuthController::class, 'refresh']);
 // app
 Route::post('/login-app', [AuthController::class, 'loginApp']);
 Route::post('/refresh-app', [AuthController::class, 'refreshApp']);
-Route::post('/user-send-code', [AuthController::class, 'sendCode']);
+Route::post('/client-send-code', [AuthController::class, 'clientSendCode']);
+Route::post('/provider-send-code', [AuthController::class, 'providerSendCode']);
 Route::post('/user-validate-code', [AuthController::class, 'validateCode']);
+Route::post('/validate-code-client', [AuthController::class, 'validateCodeClient']);
+Route::post('/validate-code-provider', [AuthController::class, 'validateCodeProvider']);
 Route::post('/register-client', [ClientController::class, 'register']);
 Route::post('/register-provider', [ProviderController::class, 'register']);