Procházet zdrojové kódy

feat(user-type): gestão da matriz de permissões por perfil (etapa 1)

- GET/PUT /user-type/{slug}/permissions: lê a árvore de módulos com os bits
  do perfil e sincroniza a matriz (Menu/Ver/Cadastrar/Editar via bitmask).
- UserTypePermissionService: bitsByPermissionId, syncPermissions (restaura
  linha soft-deleted, remove quando bits=0) e invalidação de cache por perfil.
- Remove cast UserTypeEnum de UserTypePermission.user_type: perfis dinâmicos
  (slug) criados em runtime não existem no enum e quebravam leitura/escrita.
- PermissionService::getTree (ordem de árvore + depth) para montar a matriz.
- Corrige eager load 'permissions' -> 'permission' em allGuestPermissions.

Núcleo da task 02. Próximas etapas: consolidar taxonomia/middlewares e UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ebagabee před 1 týdnem
rodič
revize
9e68409e49

+ 63 - 1
app/Http/Controllers/UserTypeController.php

@@ -2,13 +2,19 @@
 
 namespace App\Http\Controllers;
 
+use App\Services\PermissionService;
+use App\Services\UserTypePermissionService;
 use App\Services\UserTypeService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 
 class UserTypeController extends Controller
 {
-    public function __construct(protected UserTypeService $service) {}
+    public function __construct(
+        protected UserTypeService $service,
+        protected PermissionService $permissionService,
+        protected UserTypePermissionService $userTypePermissionService,
+    ) {}
 
     public function index(): JsonResponse
     {
@@ -49,4 +55,60 @@ public function destroy(int $id): JsonResponse
 
         return $this->successResponse(message: __('messages.deleted'), code: 204);
     }
+
+    /**
+     * Matriz de permissões do perfil: árvore de módulos + bits atribuídos ao perfil.
+     */
+    public function permissions(string $slug): JsonResponse
+    {
+        $userType = $this->service->findBySlug($slug);
+
+        if (!$userType) {
+            return $this->errorResponse(message: 'Tipo de usuário não encontrado.', code: 404);
+        }
+
+        $assigned = $this->userTypePermissionService->bitsByPermissionId($slug);
+
+        $permissions = $this->permissionService->getTree()->map(fn ($perm) => [
+            'id'             => $perm->id,
+            'scope'          => $perm->scope,
+            'description'    => $perm->description,
+            'parent_id'      => $perm->parent_id,
+            'depth'          => $perm->depth,
+            'available_bits' => $perm->bits,
+            'bits'           => $assigned[$perm->id] ?? 0,
+        ])->values();
+
+        return $this->successResponse(payload: [
+            'user_type' => [
+                'id'        => $userType->id,
+                'slug'      => $userType->slug,
+                'label'     => $userType->label,
+                'is_system' => $userType->is_system,
+            ],
+            'permissions' => $permissions,
+        ]);
+    }
+
+    /**
+     * Sincroniza a matriz de permissões do perfil.
+     */
+    public function updatePermissions(Request $request, string $slug): JsonResponse
+    {
+        $userType = $this->service->findBySlug($slug);
+
+        if (!$userType) {
+            return $this->errorResponse(message: 'Tipo de usuário não encontrado.', code: 404);
+        }
+
+        $data = $request->validate([
+            'permissions'                 => 'present|array',
+            'permissions.*.permission_id' => 'required|integer|exists:permissions,id',
+            'permissions.*.bits'          => 'required|integer|min:0|max:511',
+        ]);
+
+        $this->userTypePermissionService->syncPermissions($slug, $data['permissions']);
+
+        return $this->successResponse(message: __('messages.updated'));
+    }
 }

+ 3 - 3
app/Models/UserTypePermission.php

@@ -2,7 +2,6 @@
 
 namespace App\Models;
 
-use App\Enums\UserTypeEnum;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
@@ -10,7 +9,7 @@
 
 /**
  * @property int $id
- * @property UserTypeEnum $user_type
+ * @property string $user_type
  * @property int $permission_id
  * @property int $bits
  * @property \Illuminate\Support\Carbon|null $created_at
@@ -47,7 +46,8 @@ protected function casts(): array
     {
         return [
             "permission_id" => "integer",
-            "user_type" => UserTypeEnum::class,
+            // user_type é o slug (string) do perfil dinâmico em user_types; não usar
+            // UserTypeEnum aqui, pois perfis criados em runtime não existem no enum.
             "bits" => "integer",
         ];
     }

+ 9 - 0
app/Services/PermissionService.php

@@ -12,6 +12,15 @@ public function getAll(): Collection
         return Permission::orderBy("created_at", "desc")->get();
     }
 
+    /**
+     * Lista as permissões em ordem de árvore (pais antes dos filhos), com a profundidade,
+     * para montar a matriz de permissões por perfil.
+     */
+    public function getTree(): Collection
+    {
+        return Permission::defaultOrder()->withDepth()->get();
+    }
+
     public function findById(int $id): ?Permission
     {
         return Permission::find($id);

+ 73 - 4
app/Services/UserTypePermissionService.php

@@ -3,25 +3,94 @@
 namespace App\Services;
 
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
 use App\Models\UserTypePermission;
-use Cache;
 
 class UserTypePermissionService
 {
+    private const CACHE_TTL = 60 * 60 * 24;
+
     public function allGuestPermissions(): ?Collection
     {
-        return Cache::remember("permissions_guest", 60 * 60 * 24, function () {
-            return UserTypePermission::with('permissions')->where("user_type", "guest")->get();
+        return Cache::remember("permissions_guest", self::CACHE_TTL, function () {
+            return UserTypePermission::with('permission')->where("user_type", "guest")->get();
         });
     }
 
     public function allPermissionsByUserType(
         string $userType,
     ): ?Collection {
-        return Cache::remember("permissions_role_{$userType}", 60 * 60 * 24, function () use ($userType) {
+        return Cache::remember("permissions_role_{$userType}", self::CACHE_TTL, function () use ($userType) {
             return UserTypePermission::with('permission')
                 ->where('user_type', $userType)
                 ->get();
         });
     }
+
+    /**
+     * Bits atribuídos ao perfil, indexados por permission_id (para montar a matriz).
+     *
+     * @return array<int, int>
+     */
+    public function bitsByPermissionId(string $userType): array
+    {
+        return UserTypePermission::where('user_type', $userType)
+            ->pluck('bits', 'permission_id')
+            ->map(fn ($bits) => (int) $bits)
+            ->all();
+    }
+
+    /**
+     * Sincroniza a matriz de permissões de um perfil.
+     *
+     * @param array<int, array{permission_id: int, bits: int}> $items
+     */
+    public function syncPermissions(string $userType, array $items): void
+    {
+        DB::transaction(function () use ($userType, $items) {
+            foreach ($items as $item) {
+                $permissionId = (int) $item['permission_id'];
+                $bits         = (int) $item['bits'];
+
+                // withTrashed para reaproveitar linha antes soft-deleted (evita duplicatas).
+                $existing = UserTypePermission::withTrashed()
+                    ->where('user_type', $userType)
+                    ->where('permission_id', $permissionId)
+                    ->first();
+
+                if ($bits <= 0) {
+                    // Sem nenhum bit = sem acesso: remove a linha para manter a tabela enxuta.
+                    $existing?->delete();
+                    continue;
+                }
+
+                if ($existing) {
+                    if ($existing->trashed()) {
+                        $existing->restore();
+                    }
+                    $existing->bits = $bits;
+                    $existing->save();
+                    continue;
+                }
+
+                UserTypePermission::create([
+                    'user_type'     => $userType,
+                    'permission_id' => $permissionId,
+                    'bits'          => $bits,
+                ]);
+            }
+        });
+
+        $this->forgetCache($userType);
+    }
+
+    public function forgetCache(string $userType): void
+    {
+        Cache::forget("permissions_role_{$userType}");
+
+        if (strtolower($userType) === 'guest') {
+            Cache::forget("permissions_guest");
+        }
+    }
 }

+ 5 - 0
app/Services/UserTypeService.php

@@ -13,6 +13,11 @@ public function getAll(): Collection
         return UserType::orderBy('label')->get();
     }
 
+    public function findBySlug(string $slug): ?UserType
+    {
+        return UserType::where('slug', $slug)->first();
+    }
+
     public function create(array $data): UserType
     {
         $slug = strtoupper(Str::slug($data['label'], '_'));

+ 2 - 0
routes/authRoutes/user_type.php

@@ -6,5 +6,7 @@
 Route::controller(UserTypeController::class)->prefix('user-type')->group(function () {
     Route::get('/', 'index')->middleware('permission:config.user-type,view');
     Route::post('/', 'store')->middleware('permission:config.user-type,add');
+    Route::get('/{slug}/permissions', 'permissions')->middleware('permission:config.user-type,view');
+    Route::put('/{slug}/permissions', 'updatePermissions')->middleware('permission:config.user-type,edit');
     Route::delete('/{id}', 'destroy')->middleware('permission:config.user-type,delete');
 });