feat: dynamic role-based permission system

- Add permissions & role_permissions tables with migration
- Add PermissionMiddleware for route-level access control
- Add CheckRole middleware for role-based fallback
- Add PermissionController with UI (Settings > Hak Akses Role)
- Add @canPermission Blade directive for view-level checks
- Convert all routes to use dynamic permission middleware
- Add canPermission() method to User model
- All access rights configurable via admin UI
This commit is contained in:
Novy San87
2026-07-23 06:49:28 +07:00
parent 5eb3830895
commit 9c741f13f7
11 changed files with 501 additions and 74 deletions
@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class PermissionController extends Controller
{
public function index(): View
{
$permissions = DB::table('permissions')
->orderBy('group')
->orderBy('id')
->get()
->groupBy('group');
$roles = ['admin', 'bk', 'wali_kelas', 'staff'];
$rolePermissions = [];
foreach ($roles as $role) {
$rolePermissions[$role] = DB::table('role_permissions')
->where('role', $role)
->pluck('permission_id')
->toArray();
}
return view('settings.permissions', compact('permissions', 'roles', 'rolePermissions'));
}
public function update(Request $request): RedirectResponse
{
$data = $request->validate([
'permissions' => ['nullable', 'array'],
'permissions.*' => ['array'],
]);
// Clear all role_permissions first
DB::table('role_permissions')->delete();
$now = now();
if (! empty($data['permissions'])) {
foreach ($data['permissions'] as $role => $permissionIds) {
foreach ($permissionIds as $permissionId) {
DB::table('role_permissions')->insert([
'role' => $role,
'permission_id' => (int) $permissionId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
// Always give admin all permissions
$allPermissionIds = DB::table('permissions')->pluck('id');
foreach ($allPermissionIds as $pid) {
DB::table('role_permissions')->insertOrIgnore([
'role' => 'admin',
'permission_id' => $pid,
'created_at' => $now,
'updated_at' => $now,
]);
}
// Clear cache
foreach (['admin', 'bk', 'wali_kelas', 'staff'] as $role) {
Cache::forget('role_permissions:' . $role);
}
return redirect()->route('settings.permissions')
->with('success', 'Hak akses berhasil diperbarui.');
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$roles): Response
{
$user = $request->user();
if (! $user) {
abort(403, 'Unauthorized.');
}
foreach ($roles as $role) {
if ($user->role === $role) {
return $next($request);
}
}
abort(403, 'Anda tidak memiliki izin untuk mengakses halaman ini.');
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class PermissionMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$permissions): Response
{
$user = $request->user();
if (! $user) {
abort(403, 'Unauthorized.');
}
// Admin always has all permissions
if ($user->role === 'admin') {
return $next($request);
}
// Cache role permissions (cleared when role_permissions changes)
$rolePermissions = Cache::remember('role_permissions:' . $user->role, 3600, function () use ($user) {
return \DB::table('role_permissions')
->join('permissions', 'role_permissions.permission_id', '=', 'permissions.id')
->where('role_permissions.role', $user->role)
->pluck('permissions.key')
->toArray();
});
foreach ($permissions as $permission) {
if (in_array($permission, $rolePermissions)) {
return $next($request);
}
}
abort(403, 'Anda tidak memiliki izin untuk mengakses halaman ini.');
}
}
+17
View File
@@ -28,6 +28,23 @@ class User extends Authenticatable
public function isAdmin(): bool { return $this->role === 'admin'; }
public function isBK(): bool { return $this->role === 'bk'; }
public function isWaliKelas(): bool { return $this->role === 'wali_kelas'; }
public function isStaff(): bool { return $this->role === 'staff'; }
public function canPermission(string $permission): bool
{
if ($this->isAdmin()) {
return true;
}
return in_array($permission, \Illuminate\Support\Facades\Cache::remember(
'role_permissions:' . $this->role,
3600,
fn () => \DB::table('role_permissions')
->join('permissions', 'role_permissions.permission_id', '=', 'permissions.id')
->where('role_permissions.role', $this->role)
->pluck('permissions.key')
->toArray()
));
}
public function recordedViolations()
{
+21
View File
@@ -3,6 +3,8 @@
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -21,5 +23,24 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
Paginator::defaultView('vendor.pagination.tailwind');
// Blade directive: @canPermission('sync-data')
Blade::if('canPermission', function (string $permission) {
$user = auth()->user();
if (! $user) {
return false;
}
if ($user->role === 'admin') {
return true;
}
$rolePermissions = Cache::remember('role_permissions:' . $user->role, 3600, function () use ($user) {
return \DB::table('role_permissions')
->join('permissions', 'role_permissions.permission_id', '=', 'permissions.id')
->where('role_permissions.role', $user->role)
->pluck('permissions.key')
->toArray();
});
return in_array($permission, $rolePermissions);
});
}
}