Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Http\Livewire\Roles;
use Livewire\Component;
class Create extends Component
{
public function render()
{
return view('livewire.roles.create');
}
}
+622
View File
@@ -0,0 +1,622 @@
<?php
namespace App\Http\Livewire\Roles;
use App\Http\Livewire\Traits\RequiresAdminAuthorization;
use App\Models\AdminRole;
use App\Models\OrganizationRole;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;
use Spatie\Permission\PermissionRegistrar;
use WireUi\Traits\WireUiActions;
class Manage extends Component
{
use WithPagination;
use WireUiActions;
use RequiresAdminAuthorization;
public string $search = '';
public string $sortField = 'name';
public string $sortDirection = 'asc';
public int $perPage = 20;
// ── ADMIN ROLE MODALS ────────────────────────────────────────────────────
public bool $modalCreateRole = false;
public array $createRole = ['name' => '', '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'));
}
}