Files

614 lines
22 KiB
PHP

<?php
/**
* ============================================================
*
* @project biiproject
*
* @author Andika Debi Putra
*
* @email andikadebiputra@gmail.com
*
* @website https://biiproject.com
*
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
*
* @version 1.0.0
*
* @created 2026-05-01
* ============================================================
*
* Unauthorized copying, modification, distribution, or use
* of this file is strictly prohibited without prior written
* permission from the author.
* ============================================================
*/
namespace App\Http\Controllers\AccessControl;
use App\Models\Permission;
use App\Models\Role;
use App\Support\DataTable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
class RoleManagementController extends Controller
{
public function __construct()
{
// Middleware handled in web.php
}
public function index(Request $request)
{
if (DataTable::isDataTableRequest($request)) {
return $this->dataTable($request);
}
$permissions = Permission::where('is_active', 1)
->whereNull('deleted_at')
->orderBy('name')
->get();
// Categorize permissions for UI Matrix
$groupedPermissions = $this->groupPermissions($permissions);
return view('pages.access_control.roles', [
'permissions' => $permissions,
'groupedPermissions' => $groupedPermissions,
]);
}
/**
* Group permissions into a hierarchical tree: category → menu → tabs.
*
* Each entry in $groups[$category][$baseName] has:
* 'view' => Permission|null (menu-level view)
* 'manage' => Permission|null (menu-level manage)
* 'tabs' => [
* $tabSlug => ['view' => Permission|null, 'manage' => Permission|null]
* ]
*/
protected function groupPermissions($permissions)
{
$groups = [];
foreach ($permissions as $permission) {
$name = $permission->name;
// Determine category
$category = match (true) {
str_contains($name, 'ai ') => 'AI Intelligence',
str_contains($name, 'dashboard') => 'Dashboard',
str_contains($name, 'user directory') => 'User Directory',
str_contains($name, 'access rights') => 'Access Rights',
str_contains($name, 'health and logs') => 'Health & Logs',
str_contains($name, 'action history') => 'Action History',
str_contains($name, 'global settings') => 'Global Settings',
str_contains($name, 'mobile settings') => 'Mobile Settings',
str_contains($name, 'active sessions') => 'Active Sessions',
str_contains($name, 'notification center') => 'Notification Center',
str_contains($name, 'impersonate') => 'Impersonation',
str_contains($name, 'backup') || str_contains($name, 'maintenance') => 'System Config',
default => 'Other',
};
// Strip action prefix
$type = 'other';
$resource = $name;
if (str_starts_with($name, 'manage ')) {
$type = 'manage';
$resource = substr($name, 7);
} elseif (str_starts_with($name, 'view ')) {
$type = 'view';
$resource = substr($name, 5);
}
// Detect scoped (tab-level) permission: "resource:tab-slug"
if (str_contains($resource, ':')) {
[$menu, $tab] = explode(':', $resource, 2);
$menu = trim($menu);
$tab = trim($tab);
if (! isset($groups[$category][$menu])) {
$groups[$category][$menu] = ['view' => null, 'manage' => null, 'tabs' => []];
}
if (! isset($groups[$category][$menu]['tabs'][$tab])) {
$groups[$category][$menu]['tabs'][$tab] = ['view' => null, 'manage' => null];
}
$groups[$category][$menu]['tabs'][$tab][$type] = $permission;
} else {
$menu = trim($resource);
if (! isset($groups[$category][$menu])) {
$groups[$category][$menu] = ['view' => null, 'manage' => null, 'tabs' => []];
}
$groups[$category][$menu][$type] = $permission;
}
}
// Sort categories
$priority = ['Dashboard', 'User Directory', 'Access Rights', 'AI Intelligence', 'Health & Logs', 'Action History', 'Global Settings', 'Mobile Settings', 'Notification Center', 'Active Sessions', 'Impersonation', 'System Config'];
uksort($groups, function ($a, $b) use ($priority) {
$posA = array_search($a, $priority);
$posB = array_search($b, $priority);
if ($posA === false && $posB === false) {
return strcmp($a, $b);
}
return ($posA === false) ? 1 : (($posB === false) ? -1 : $posA - $posB);
});
return $groups;
}
protected function dataTable(Request $request)
{
try {
$authUser = $request->user();
$canManage = $authUser->can('manage access rights');
$query = Role::query()->withTrashed()->with(['permissions:id,name', 'creator:id,email', 'updater:id,email']);
$globalSearch = DataTable::globalSearch($request);
$trashed = $request->input('trashed');
if ($trashed === 'archived') {
$query->onlyTrashed();
} elseif ($trashed === 'active') {
$query->withoutTrashed();
}
if ($canManage) {
$status = DataTable::columnSearch($request, 1);
$name = DataTable::columnSearch($request, 2);
$guard = DataTable::columnSearch($request, 3);
$permission = DataTable::columnSearch($request, 4);
} else {
$status = null;
$name = DataTable::columnSearch($request, 0);
$guard = DataTable::columnSearch($request, 1);
$permission = DataTable::columnSearch($request, 2);
}
if ($status) {
$query->where('is_active', $status === 'active');
}
if ($name) {
$query->where('name', 'like', "%{$name}%");
}
if ($guard) {
$query->where('guard_name', 'like', "%{$guard}%");
}
if ($permission) {
$query->whereHas('permissions', function ($permissionQuery) use ($permission) {
$permissionQuery->where('name', 'like', "%{$permission}%");
});
}
// Audit columns (fixed indices from the end or just using canManage logic)
if ($canManage) {
$createdAt = DataTable::columnSearch($request, 5);
$createdBy = DataTable::columnSearch($request, 6);
$updatedAt = DataTable::columnSearch($request, 7);
$updatedBy = DataTable::columnSearch($request, 8);
} else {
$createdAt = DataTable::columnSearch($request, 3);
$createdBy = DataTable::columnSearch($request, 4);
$updatedAt = DataTable::columnSearch($request, 5);
$updatedBy = DataTable::columnSearch($request, 6);
}
if ($createdAt) {
$query->whereDate('created_at', $createdAt);
}
if ($createdBy) {
$query->whereHas('creator', function ($creatorQuery) use ($createdBy) {
$creatorQuery->where('email', 'like', "%{$createdBy}%");
});
}
if ($updatedAt) {
$query->whereDate('updated_at', $updatedAt);
}
if ($updatedBy) {
$query->whereHas('updater', function ($updaterQuery) use ($updatedBy) {
$updaterQuery->where('email', 'like', "%{$updatedBy}%");
});
}
if ($globalSearch) {
$query->where(function ($searchQuery) use ($globalSearch) {
$searchQuery
->where('name', 'like', "%{$globalSearch}%")
->orWhere('guard_name', 'like', "%{$globalSearch}%")
->orWhereHas('permissions', function ($permissionQuery) use ($globalSearch) {
$permissionQuery->where('name', 'like', "%{$globalSearch}%");
})
->orWhereHas('creator', function ($creatorQuery) use ($globalSearch) {
$creatorQuery->where('email', 'like', "%{$globalSearch}%");
})
->orWhereHas('updater', function ($updaterQuery) use ($globalSearch) {
$updaterQuery->where('email', 'like', "%{$globalSearch}%");
});
});
}
// Perform filtered count WITHOUT eager loading or ordering
$countQuery = clone $query;
$countQuery->setEagerLoads([]);
$countQuery->orders = null;
$recordsFiltered = $countQuery->count();
$recordsTotal = Role::withTrashed()->count(); // Total across all states without eager loads
[$orderIndex, $orderDirection] = DataTable::order($request, $canManage ? 5 : 3, 'desc');
$sortColumn = match (true) {
$canManage && $orderIndex === 1 => 'is_active',
($canManage && $orderIndex === 2) || (! $canManage && $orderIndex === 0) => 'name',
($canManage && $orderIndex === 3) || (! $canManage && $orderIndex === 1) => 'guard_name',
($canManage && $orderIndex === 5) || (! $canManage && $orderIndex === 3) => 'created_at',
($canManage && $orderIndex === 7) || (! $canManage && $orderIndex === 5) => 'updated_at',
default => 'created_at',
};
$roles = $query
->orderBy($sortColumn, $orderDirection)
->skip(DataTable::start($request))
->take(DataTable::length($request))
->get();
$rows = $roles->map(function (Role $role) use ($canManage) {
$permissionNames = $role->permissions->pluck('name')->values();
$permissionBadges = $permissionNames->isNotEmpty()
? $permissionNames->map(fn ($permission) => '<span class="badge badge-slate-pill">'.e($permission).'</span>')->implode(' ')
: '-';
$row = [];
if ($canManage) {
$row[] = sprintf(
'<input type="checkbox" class="form-check-input role-checkbox" value="%d">',
$role->id
);
$row[] = sprintf(
'<div class="form-check form-switch d-flex align-items-center gap-2"><input class="form-check-input role-toggle" type="checkbox" %s data-id="%d" data-name="%s"><span class="status-label %s">%s</span></div>',
$role->is_active ? 'checked' : '',
$role->id,
e($role->name),
$role->is_active ? 'text-success' : 'text-danger',
$role->is_active ? 'Active' : 'Inactive'
);
}
$row[] = e($role->name);
$row[] = e($role->guard_name);
$row[] = '<div class="permission-clamp" title="'.e($permissionNames->implode(', ')).'">'.$permissionBadges.'</div>';
$row[] = e(format_datetime($role->created_at));
$row[] = e($role->creator->email ?? 'System');
$row[] = e(format_datetime($role->updated_at));
$row[] = e($role->updater->email ?? '-');
if ($canManage) {
if ($role->trashed()) {
$row[] = '<div class="text-end d-inline-flex gap-2 justify-content-end w-100">'
.'<button class="btn btn-link text-success btn-restore" data-id="'.$role->id.'" data-name="'.e($role->name).'"><i class="bi bi-arrow-counterclockwise"></i> '.e(__('Restore')).'</button>'
.'<button class="btn btn-link text-danger btn-force-delete" data-id="'.$role->id.'" data-name="'.e($role->name).'"><i class="bi bi-x-circle"></i> '.e(__('Terminate')).'</button>'
.'</div>';
} else {
$row[] = '<div class="text-end d-inline-flex gap-2 justify-content-end w-100">'
.'<button class="btn btn-link btn-edit" data-id="'.$role->id.'" data-name="'.e($role->name).'" data-guard="'.e($role->guard_name).'" data-permissions="'.e(json_encode($role->permissions->pluck('id')->values()->all())).'" data-created="'.e(format_datetime($role->created_at)).'" data-created-by="'.e($role->creator->email ?? 'System').'" data-updated-at="'.e(format_datetime($role->updated_at)).'" data-updated-by="'.e($role->updater->email ?? '-').'" data-bs-toggle="modal" data-bs-target="#editRoleModal"><i class="bi bi-pencil"></i></button>'
.'<button class="btn btn-link text-danger btn-delete" type="button" data-id="'.$role->id.'" data-name="'.e($role->name).'"><i class="bi bi-trash"></i></button>'
.'</div>';
}
}
return $row;
})->all();
return DataTable::response($request, $recordsTotal, $recordsFiltered, $rows);
} catch (\Exception $e) {
Log::error('DataTable Error [RoleManagement]: '.$e->getMessage());
return DataTable::response($request, 0, 0, []);
}
}
public function store(Request $request)
{
$request->validate([
'name' => [
'required',
'string',
'min:3',
'max:50',
'regex:/^[a-zA-Z0-9_\-]+$/',
Rule::unique('roles', 'name'),
],
'guard_name' => [
'required',
'in:web,api',
],
'permissions' => [
'required',
'array',
'min:1',
],
'permissions.*' => [
Rule::exists('permissions', 'id'),
],
]);
try {
$role = Role::create([
'name' => $request->name,
'guard_name' => $request->guard_name,
'created_by' => Auth::id(),
]);
$permissionNames = Permission::whereIn('id', $request->permissions)
->pluck('name')
->toArray();
$role->syncPermissions($permissionNames);
if ($request->expectsJson()) {
return response()->json([
'success' => true,
'message' => __('Role has been successfully created and permissions assigned.'),
]);
}
return back()->with('success', __('Role has been successfully created and permissions assigned.'));
} catch (\Exception $e) {
if ($request->expectsJson()) {
return response()->json([
'success' => false,
'message' => __('Failed to create role: ').$e->getMessage(),
], 500);
}
return back()->with('error', __('Failed to create role: ').$e->getMessage());
}
}
public function update(Request $request, $id)
{
$request->validate([
'name' => [
'required',
'string',
'min:3',
'max:50',
'regex:/^[a-zA-Z0-9_\-]+$/',
Rule::unique('roles', 'name')->ignore($id),
],
'guard_name' => [
'required',
'in:web,api',
],
'permissions' => [
'required',
'array',
'min:1',
],
'permissions.*' => [
Rule::exists('permissions', 'id'),
],
]);
try {
$role = Role::findOrFail($id);
$role->update([
'name' => $request->name,
'guard_name' => $request->guard_name,
'updated_by' => Auth::id(),
]);
$permissionNames = Permission::whereIn('id', $request->permissions)
->pluck('name')
->toArray();
$role->syncPermissions($permissionNames);
if ($request->expectsJson()) {
return response()->json([
'success' => true,
'message' => __('Role successfully updated.'),
]);
}
return back()->with('success', __('Role successfully updated.'));
} catch (\Exception $e) {
if ($request->expectsJson()) {
return response()->json([
'success' => false,
'message' => __('Failed to update role: ').$e->getMessage(),
], 500);
}
return back()->with('error', __('Failed to update role: ').$e->getMessage());
}
}
public function toggleStatus(Request $request)
{
$request->validate([
'id' => 'required|exists:roles,id',
'status' => 'required|in:activate,deactivate',
]);
$role = Role::findOrFail($request->id);
$role->update([
'is_active' => $request->status === 'activate' ? 1 : 0,
'updated_by' => Auth::id(),
]);
return response()->json([
'success' => true,
'message' => __('Role status updated successfully.'),
]);
}
public function destroy($id)
{
$role = Role::findOrFail($id);
// Check if role is still used by users
if ($role->users()->exists()) {
return response()->json([
'success' => false,
'message' => __('Cannot archive. This role is still assigned to some users.'),
], 422);
}
$role->delete();
return response()->json([
'success' => true,
'message' => __('Role has been archived.'),
]);
}
/**
* RESTORE — Restore role (Soft Delete)
*/
public function restore($id)
{
$role = Role::withTrashed()->findOrFail($id);
$role->restore();
return response()->json([
'success' => true,
'message' => __('Role has been restored.'),
]);
}
/**
* FORCE DELETE — Permanent removal
*/
public function forceDelete($id)
{
$role = Role::withTrashed()->findOrFail($id);
// Final usage check before permanent deletion
if ($role->users()->exists()) {
return response()->json([
'success' => false,
'message' => __('Cannot delete permanently. This role is still assigned to some users.'),
], 422);
}
$role->forceDelete();
return response()->json([
'success' => true,
'message' => __('Role has been permanently removed.'),
]);
}
public function bulkToggleStatus(Request $request)
{
$request->validate([
'ids' => 'required|array',
'ids.*' => 'exists:roles,id',
'status' => 'required|in:activate,deactivate',
]);
Role::whereIn('id', $request->ids)->update([
'is_active' => $request->status === 'activate' ? 1 : 0,
'updated_by' => Auth::id(),
]);
return response()->json([
'success' => true,
'message' => __('Selected roles status updated successfully.'),
]);
}
public function bulkDelete(Request $request)
{
$request->validate([
'ids' => 'required|array',
'ids.*' => 'exists:roles,id',
]);
$roles = Role::whereIn('id', $request->ids)->get();
$archivedCount = 0;
$failedIds = [];
foreach ($roles as $role) {
if (! $role->users()->exists()) {
$role->delete();
$archivedCount++;
} else {
$failedIds[] = $role->name;
}
}
$message = __(':count roles archived.', ['count' => $archivedCount]);
if (! empty($failedIds)) {
$message .= ' '.__('Could not archive :names because they are still assigned to users.', ['names' => implode(', ', $failedIds)]);
}
return response()->json([
'success' => true,
'message' => $message,
]);
}
public function bulkRestore(Request $request)
{
$request->validate([
'ids' => 'required|array',
'ids.*' => 'exists:roles,id',
]);
Role::withTrashed()->whereIn('id', $request->ids)->restore();
return response()->json([
'success' => true,
'message' => __('Selected roles restored successfully.'),
]);
}
public function bulkForceDelete(Request $request)
{
$request->validate([
'ids' => 'required|array',
'ids.*' => 'exists:roles,id',
]);
$roles = Role::withTrashed()->whereIn('id', $request->ids)->get();
$deletedCount = 0;
$failedIds = [];
foreach ($roles as $role) {
if (! $role->users()->exists()) {
$role->forceDelete();
$deletedCount++;
} else {
$failedIds[] = $role->name;
}
}
$message = __(':count roles permanently removed.', ['count' => $deletedCount]);
if (! empty($failedIds)) {
$message .= ' '.__('Could not remove :names because they are still assigned to users.', ['names' => implode(', ', $failedIds)]);
}
return response()->json([
'success' => true,
'message' => $message,
]);
}
}