'', 'label' => '', 'permissions' => []]; public bool $modalEditRole = false; #[Locked] public ?int $editRoleId = null; #[Locked] public bool $editRoleIsSystem = false; public array $editRole = ['name' => '', 'label' => '', 'permissions' => []]; public bool $modalDeleteRole = false; #[Locked] public ?int $deleteRoleId = null; public string $deleteRoleName = ''; #[Locked] public array $deleteAffectedUsers = []; // ── ORG/BANK ROLE MODALS ───────────────────────────────────────────────── public bool $modalCreateOrgRole = false; #[Locked] public string $createOrgRoleType = 'organization'; // 'organization' or 'bank' public array $createOrgRole = ['name' => '', 'label' => '', 'permissions' => []]; public bool $modalEditOrgRole = false; #[Locked] public ?int $editOrgRoleId = null; #[Locked] public bool $editOrgRoleIsSystem = false; public array $editOrgRole = ['name' => '', 'label' => '', 'permissions' => []]; public bool $modalDeleteOrgRole = false; #[Locked] public ?int $deleteOrgRoleId = null; public string $deleteOrgRoleName = ''; #[Locked] public string $deleteOrgRoleType = 'organization'; #[Locked] public array $deleteOrgAffectedUsers = []; // ── AVAILABLE PERMISSIONS ──────────────────────────────────────────────── #[Locked] public array $availablePermissions = [ // Posts 'view posts', 'create posts', 'update posts', 'publish posts', 'unpublish posts', 'delete posts', 'manage posts', // Polls ('export' = who voted and the CSV export) 'view polls', 'create polls', 'update polls', 'export polls', 'delete polls', 'manage polls', // Tags 'view tags', 'create tags', 'update tags', 'delete tags', 'manage tags', // Categories 'view categories', 'create categories', 'update categories', 'delete categories', 'manage categories', // Calls 'view calls', 'create calls', 'update calls', 'publish calls', 'unpublish calls', 'delete calls', 'manage calls', // Mailings 'view mailings', 'create mailings', 'update mailings', 'send mailings', 'delete mailings', 'manage mailings', // Users 'view users', 'create users', 'update users', 'delete users', 'manage users', // User projects 'view user_projects', 'create user_projects', 'update user_projects', 'delete user_projects', 'manage user_projects', // Organizations 'view organizations', 'create organizations', 'update organizations', 'delete organizations', 'manage organizations', // Banks 'view banks', 'create banks', 'update banks', 'delete banks', 'manage banks', // Admins 'view admins', 'create admins', 'update admins', 'delete admins', 'manage admins', // Accounts 'view accounts', 'create accounts', 'update accounts', 'delete accounts', 'manage accounts', // Profiles 'view profiles', 'create profiles', 'update profiles', 'delete profiles', 'manage profiles', // Permissions 'view permissions', 'create permissions', 'update permissions', 'delete permissions', 'manage permissions', // Roles 'view roles', 'create roles', 'update roles', 'delete roles', 'manage roles', // Maintenance 'manage maintenance', // Admin logs & metrics 'view search-metrics', 'view application-log', 'view inactive-profiles-log', 'view search-reindex-log', // Platform statistics 'view stats', // Admin profile capabilities 'admin reports', 'admin contacts', 'admin messenger', 'admin settings', 'admin change-password', ]; #[Locked] public array $availableOrgPermissions = [ 'org pay', 'org transactions', 'org reports', 'org stats', 'org contacts', 'org calls', 'org messenger', 'org edit-profile', 'org settings', 'org change-password', ]; protected $queryString = [ 'search' => ['except' => ''], 'sortField' => ['except' => 'name'], 'sortDirection' => ['except' => 'asc'], ]; public function mount() { $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); if (!$activeProfileType || !$activeProfileId) { abort(403, __('No active profile selected')); } $profile = $activeProfileType::find($activeProfileId); if (!$profile) { abort(403, __('Active profile not found')); } \App\Helpers\ProfileAuthorizationHelper::authorize($profile); if (!($profile instanceof \App\Models\Admin)) { abort(403, __('Admin access required')); } if (!Gate::allows('access roles')) { abort(403, __('You do not have permission to access roles.')); } \Log::info('Roles management access', [ 'component' => 'Roles\\Manage', 'profile_id' => $profile->id, 'profile_type' => get_class($profile), 'authenticated_guard' => \Auth::getDefaultDriver(), 'ip_address' => request()->ip(), ]); } public function updatingSearch() { $this->resetPage(); } public function resetSearch() { $this->search = ''; $this->resetPage(); } public function sortBy(string $field) { if ($this->sortField === $field) { $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { $this->sortField = $field; $this->sortDirection = 'asc'; } } // ── ADMIN CREATE ───────────────────────────────────────────────────────── public function openCreateModal() { $this->authorizePermission('create roles'); $this->createRole = ['name' => '', 'label' => '', 'permissions' => []]; $this->resetErrorBag(); $this->modalCreateRole = true; } public function saveCreate() { $this->authorizePermission('create roles'); $this->validate([ 'createRole.name' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9\-]+$/', 'unique:admin_roles,name'], 'createRole.label' => ['required', 'string', 'max:128'], 'createRole.permissions' => ['required', 'array', 'min:1'], 'createRole.permissions.*' => ['string', 'in:' . implode(',', $this->availablePermissions)], ], [ 'createRole.name.regex' => __('Name may only contain lowercase letters, numbers, and hyphens.'), 'createRole.name.unique' => __('A role with this name already exists.'), ]); DB::beginTransaction(); try { $role = new AdminRole([ 'name' => $this->createRole['name'], 'label' => $this->createRole['label'], 'permissions' => $this->createRole['permissions'], ]); $role->is_system = false; $role->save(); $this->syncAdminSpatieRolePermissions($role); DB::commit(); app()[PermissionRegistrar::class]->forgetCachedPermissions(); $this->modalCreateRole = false; $this->notification()->success(__('Success!'), __('Role created.')); } catch (\Exception $e) { DB::rollBack(); $this->notification()->error(__('Error!'), $e->getMessage()); } } // ── ADMIN EDIT ─────────────────────────────────────────────────────────── public function openEditModal(int $id) { $this->authorizePermission('update roles'); $role = AdminRole::find($id); if (!$role) { $this->notification()->error(__('Error!'), __('Role not found.')); return; } $this->editRoleId = $id; $this->editRoleIsSystem = (bool) $role->is_system; $this->editRole = [ 'name' => $role->name, 'label' => $role->label, 'permissions' => $role->permissions ?? [], ]; $this->resetErrorBag(); $this->modalEditRole = true; } public function saveEdit() { $this->authorizePermission('update roles'); $this->validate([ 'editRole.label' => ['required', 'string', 'max:128'], 'editRole.permissions' => ['required', 'array', 'min:1'], 'editRole.permissions.*' => ['string', 'in:' . implode(',', $this->availablePermissions)], ]); $role = AdminRole::find($this->editRoleId); if (!$role) { $this->notification()->error(__('Error!'), __('Role not found.')); return; } DB::beginTransaction(); try { $role->update([ 'label' => $this->editRole['label'], 'permissions' => $this->editRole['permissions'], ]); $this->syncAdminSpatieRolePermissions($role); DB::commit(); app()[PermissionRegistrar::class]->forgetCachedPermissions(); $this->modalEditRole = false; $this->notification()->success(__('Success!'), __('Role updated.')); } catch (\Exception $e) { DB::rollBack(); $this->notification()->error(__('Error!'), $e->getMessage()); } } // ── ADMIN DELETE ───────────────────────────────────────────────────────── public function openDeleteModal(int $id) { $this->authorizePermission('delete roles'); $role = AdminRole::find($id); if (!$role) { $this->notification()->error(__('Error!'), __('Role not found.')); return; } if ($role->is_system) { $this->notification()->error(__('Error!'), __('System roles cannot be deleted.')); return; } $this->deleteRoleId = $id; $this->deleteRoleName = $role->label; $this->deleteAffectedUsers = $this->resolveAffectedUsers($role->name, 'Admin'); $this->modalDeleteRole = true; } public function deleteRole() { $this->authorizePermission('delete roles'); $role = AdminRole::find($this->deleteRoleId); if (!$role || $role->is_system) { $this->modalDeleteRole = false; return; } DB::beginTransaction(); try { $suffix = '\\' . $role->name; $spatieRoles = \Spatie\Permission\Models\Role::where('guard_name', 'web')->get() ->filter(fn($r) => str_ends_with($r->name, $suffix)); foreach ($spatieRoles as $spatieRole) { // Parse Admin\{id}\{roleName} — extract admin ID $parts = explode('\\', $spatieRole->name); $adminId = $parts[1] ?? null; if ($adminId) { // Find all users assigned this scoped role and detach from the admin profile $userIds = DB::table('model_has_roles') ->where('role_id', $spatieRole->id) ->where('model_type', \App\Models\User::class) ->pluck('model_id'); if ($userIds->isNotEmpty()) { \App\Models\Admin::find($adminId)?->users()->detach($userIds->toArray()); } } $spatieRole->delete(); } $role->delete(); DB::commit(); app()[PermissionRegistrar::class]->forgetCachedPermissions(); $this->modalDeleteRole = false; $this->notification()->success(__('Success!'), __('Role deleted.')); } catch (\Exception $e) { DB::rollBack(); $this->notification()->error(__('Error!'), $e->getMessage()); } } // ── ORG CREATE ─────────────────────────────────────────────────────────── public function openCreateOrgRoleModal(string $type = 'organization') { $this->authorizePermission('create roles'); $this->createOrgRoleType = in_array($type, ['organization', 'bank']) ? $type : 'organization'; $this->createOrgRole = ['name' => '', 'label' => '', 'permissions' => []]; $this->resetErrorBag(); $this->modalCreateOrgRole = true; } public function saveCreateOrgRole() { $this->authorizePermission('create roles'); $prefix = $this->createOrgRoleType . '-'; $this->validate([ 'createOrgRole.name' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9\-]+$/', 'unique:organization_roles,name', function ($attribute, $value, $fail) use ($prefix) { if (!str_starts_with($value, $prefix)) { $fail(__("Role name must start with \"{$prefix}\".")); } }, ], 'createOrgRole.label' => ['required', 'string', 'max:128'], 'createOrgRole.permissions' => ['required', 'array', 'min:1'], 'createOrgRole.permissions.*' => ['string', 'in:' . implode(',', $this->availableOrgPermissions)], ], [ 'createOrgRole.name.regex' => __('Name may only contain lowercase letters, numbers, and hyphens.'), 'createOrgRole.name.unique' => __('A role with this name already exists.'), ]); DB::beginTransaction(); try { $role = new OrganizationRole([ 'name' => $this->createOrgRole['name'], 'label' => $this->createOrgRole['label'], 'permissions' => $this->createOrgRole['permissions'], ]); $role->is_system = false; $role->save(); $this->syncOrgSpatieRolePermissions($role); DB::commit(); app()[PermissionRegistrar::class]->forgetCachedPermissions(); $this->modalCreateOrgRole = false; $this->notification()->success(__('Success!'), __('Role created.')); } catch (\Exception $e) { DB::rollBack(); $this->notification()->error(__('Error!'), $e->getMessage()); } } // ── ORG EDIT ───────────────────────────────────────────────────────────── public function openEditOrgRoleModal(int $id) { $this->authorizePermission('update roles'); $role = OrganizationRole::find($id); if (!$role) { $this->notification()->error(__('Error!'), __('Role not found.')); return; } $this->editOrgRoleId = $id; $this->editOrgRoleIsSystem = (bool) $role->is_system; $this->editOrgRole = [ 'name' => $role->name, 'label' => $role->label, 'permissions' => $role->permissions ?? [], ]; $this->resetErrorBag(); $this->modalEditOrgRole = true; } public function saveEditOrgRole() { $this->authorizePermission('update roles'); $this->validate([ 'editOrgRole.label' => ['required', 'string', 'max:128'], 'editOrgRole.permissions' => ['required', 'array', 'min:1'], 'editOrgRole.permissions.*' => ['string', 'in:' . implode(',', $this->availableOrgPermissions)], ]); $role = OrganizationRole::find($this->editOrgRoleId); if (!$role) { $this->notification()->error(__('Error!'), __('Role not found.')); return; } DB::beginTransaction(); try { $role->update([ 'label' => $this->editOrgRole['label'], 'permissions' => $this->editOrgRole['permissions'], ]); $this->syncOrgSpatieRolePermissions($role); DB::commit(); app()[PermissionRegistrar::class]->forgetCachedPermissions(); $this->modalEditOrgRole = false; $this->notification()->success(__('Success!'), __('Role updated.')); } catch (\Exception $e) { DB::rollBack(); $this->notification()->error(__('Error!'), $e->getMessage()); } } // ── ORG DELETE ─────────────────────────────────────────────────────────── public function openDeleteOrgRoleModal(int $id) { $this->authorizePermission('delete roles'); $role = OrganizationRole::find($id); if (!$role) { $this->notification()->error(__('Error!'), __('Role not found.')); return; } if ($role->is_system) { $this->notification()->error(__('Error!'), __('System roles cannot be deleted.')); return; } $this->deleteOrgRoleId = $id; $this->deleteOrgRoleName = $role->label; $this->deleteOrgRoleType = str_starts_with($role->name, 'bank-') ? 'bank' : 'organization'; $this->deleteOrgAffectedUsers = $this->resolveAffectedUsers($role->name, $this->deleteOrgRoleType === 'bank' ? 'Bank' : 'Organization'); $this->modalDeleteOrgRole = true; } public function deleteOrgRole() { $this->authorizePermission('delete roles'); $role = OrganizationRole::find($this->deleteOrgRoleId); if (!$role || $role->is_system) { $this->modalDeleteOrgRole = false; return; } DB::beginTransaction(); try { $suffix = '\\' . $role->name; $isBank = str_starts_with($role->name, 'bank-'); $spatieRoles = \Spatie\Permission\Models\Role::where('guard_name', 'web')->get() ->filter(fn($r) => str_ends_with($r->name, $suffix)); foreach ($spatieRoles as $spatieRole) { // Parse Organization\{id}\{roleName} or Bank\{id}\{roleName} $parts = explode('\\', $spatieRole->name); $profileId = $parts[1] ?? null; if ($profileId) { $userIds = DB::table('model_has_roles') ->where('role_id', $spatieRole->id) ->where('model_type', \App\Models\User::class) ->pluck('model_id'); if ($userIds->isNotEmpty()) { if ($isBank) { \App\Models\Bank::find($profileId)?->managers()->detach($userIds->toArray()); } else { \App\Models\Organization::find($profileId)?->managers()->detach($userIds->toArray()); } } } $spatieRole->delete(); } $role->delete(); DB::commit(); app()[PermissionRegistrar::class]->forgetCachedPermissions(); $this->modalDeleteOrgRole = false; $this->notification()->success(__('Success!'), __('Role deleted.')); } catch (\Exception $e) { DB::rollBack(); $this->notification()->error(__('Error!'), $e->getMessage()); } } // ── HELPERS ────────────────────────────────────────────────────────────── protected function resolveAffectedUsers(string $roleName, string $profileType): array { $suffix = '\\' . $roleName; $spatieRoles = \Spatie\Permission\Models\Role::where('guard_name', 'web')->get() ->filter(fn($r) => str_ends_with($r->name, $suffix) && str_starts_with($r->name, $profileType . '\\')); $affected = []; foreach ($spatieRoles as $spatieRole) { $parts = explode('\\', $spatieRole->name); $profileId = $parts[1] ?? null; $userIds = DB::table('model_has_roles') ->where('role_id', $spatieRole->id) ->where('model_type', \App\Models\User::class) ->pluck('model_id'); foreach ($userIds as $userId) { $user = \App\Models\User::find($userId); if (!$user) continue; $profileLabel = $profileType . ' #' . $profileId; if ($profileType === 'Admin') { $profileLabel = \App\Models\Admin::find($profileId)?->name ?? $profileLabel; } elseif ($profileType === 'Bank') { $profileLabel = \App\Models\Bank::find($profileId)?->name ?? $profileLabel; } elseif ($profileType === 'Organization') { $profileLabel = \App\Models\Organization::find($profileId)?->name ?? $profileLabel; } $affected[] = ['user' => $user->name, 'profile' => $profileLabel]; } } return $affected; } protected function syncAdminSpatieRolePermissions(AdminRole $adminRole) { $suffix = '\\' . $adminRole->name; \Spatie\Permission\Models\Role::where('guard_name', 'web')->get() ->filter(fn($r) => str_ends_with($r->name, $suffix)) ->each(fn($r) => $r->syncPermissions($adminRole->permissions)); } protected function syncOrgSpatieRolePermissions(OrganizationRole $orgRole) { $suffix = '\\' . $orgRole->name; \Spatie\Permission\Models\Role::where('guard_name', 'web')->get() ->filter(fn($r) => str_ends_with($r->name, $suffix)) ->each(fn($r) => $r->syncPermissions($orgRole->permissions)); } public function render() { $query = AdminRole::query(); if ($this->search) { $query->where(function ($q) { $q->where('name', 'like', '%' . $this->search . '%') ->orWhere('label', 'like', '%' . $this->search . '%'); }); } $allowedSorts = ['name', 'label']; $sortField = in_array($this->sortField, $allowedSorts) ? $this->sortField : 'name'; $sortDirection = $this->sortDirection === 'desc' ? 'desc' : 'asc'; $roles = $query->orderBy($sortField, $sortDirection)->paginate($this->perPage); $orgRoles = OrganizationRole::where('name', 'like', 'organization-%')->orderBy('is_system', 'desc')->orderBy('name')->get(); $bankRoles = OrganizationRole::where('name', 'like', 'bank-%')->orderBy('is_system', 'desc')->orderBy('name')->get(); return view('livewire.roles.manage', compact('roles', 'orgRoles', 'bankRoles')); } }