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:
@@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,23 @@ class User extends Authenticatable
|
|||||||
public function isAdmin(): bool { return $this->role === 'admin'; }
|
public function isAdmin(): bool { return $this->role === 'admin'; }
|
||||||
public function isBK(): bool { return $this->role === 'bk'; }
|
public function isBK(): bool { return $this->role === 'bk'; }
|
||||||
public function isWaliKelas(): bool { return $this->role === 'wali_kelas'; }
|
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()
|
public function recordedViolations()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Pagination\Paginator;
|
use Illuminate\Pagination\Paginator;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -21,5 +23,24 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Paginator::defaultView('vendor.pagination.tailwind');
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -12,7 +12,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
$middleware->alias([
|
||||||
|
'role' => \App\Http\Middleware\CheckRole::class,
|
||||||
|
'permission' => \App\Http\Middleware\PermissionMiddleware::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
$exceptions->shouldRenderJsonWhen(
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('permissions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('key', 100)->unique();
|
||||||
|
$table->string('group', 50);
|
||||||
|
$table->string('label', 100);
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('role_permissions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('role', 50);
|
||||||
|
$table->foreignId('permission_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['role', 'permission_id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed default permissions
|
||||||
|
$permissions = [
|
||||||
|
// Master Data (admin + bk default)
|
||||||
|
['categories-manage', 'master-data', 'Kelola Kategori Pelanggaran'],
|
||||||
|
['violation-types-manage', 'master-data', 'Kelola Jenis Pelanggaran'],
|
||||||
|
['thresholds-manage', 'master-data', 'Kelola Ambang SP'],
|
||||||
|
['violations-export', 'master-data', 'Export Data Pelanggaran'],
|
||||||
|
|
||||||
|
// Administrasi (admin only default)
|
||||||
|
['settings-manage', 'administrasi', 'Pengaturan Aplikasi'],
|
||||||
|
['users-manage', 'administrasi', 'Manajemen User'],
|
||||||
|
['sync-data', 'administrasi', 'Sinkronisasi Data'],
|
||||||
|
['backup-database', 'administrasi', 'Backup Database'],
|
||||||
|
['reset-application', 'administrasi', 'Reset Aplikasi'],
|
||||||
|
['import-data', 'administrasi', 'Import Data'],
|
||||||
|
['export-master', 'administrasi', 'Export Master Data'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
foreach ($permissions as [$key, $group, $label]) {
|
||||||
|
DB::table('permissions')->insert([
|
||||||
|
'key' => $key,
|
||||||
|
'group' => $group,
|
||||||
|
'label' => $label,
|
||||||
|
'description' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: admin gets all permissions, bk gets master-data
|
||||||
|
$adminRole = 'admin';
|
||||||
|
$bkRole = 'bk';
|
||||||
|
$masterKeys = ['categories-manage', 'violation-types-manage', 'thresholds-manage', 'violations-export'];
|
||||||
|
|
||||||
|
foreach ($permissions as [$key, $group, $label]) {
|
||||||
|
DB::table('role_permissions')->insert([
|
||||||
|
'role' => $adminRole,
|
||||||
|
'permission_id' => DB::table('permissions')->where('key', $key)->value('id'),
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (in_array($key, $masterKeys)) {
|
||||||
|
DB::table('role_permissions')->insert([
|
||||||
|
'role' => $bkRole,
|
||||||
|
'permission_id' => DB::table('permissions')->where('key', $key)->value('id'),
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('role_permissions');
|
||||||
|
Schema::dropIfExists('permissions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -20,6 +20,8 @@ $faIcons = [
|
|||||||
'cog' => 'fa-solid fa-gear',
|
'cog' => 'fa-solid fa-gear',
|
||||||
'users-cog' => 'fa-solid fa-user-gear',
|
'users-cog' => 'fa-solid fa-user-gear',
|
||||||
'database' => 'fa-solid fa-database',
|
'database' => 'fa-solid fa-database',
|
||||||
|
'arrows-rotate' => 'fa-solid fa-arrows-rotate',
|
||||||
|
'lock' => 'fa-solid fa-lock',
|
||||||
];
|
];
|
||||||
$faClass = $faIcons[$icon] ?? 'fa-solid fa-circle';
|
$faClass = $faIcons[$icon] ?? 'fa-solid fa-circle';
|
||||||
@endphp
|
@endphp
|
||||||
|
|||||||
@@ -44,24 +44,55 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<nav class="mt-2 px-2 space-y-1">
|
<nav class="mt-2 px-2 space-y-1">
|
||||||
|
@canPermission('access-dashboard')
|
||||||
<x-nav-item href="{{ route('dashboard') }}" icon="home" :active="request()->routeIs('dashboard')">Dashboard</x-nav-item>
|
<x-nav-item href="{{ route('dashboard') }}" icon="home" :active="request()->routeIs('dashboard')">Dashboard</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('input-violations')
|
||||||
<x-nav-item href="{{ route('violations.create') }}" icon="plus-circle" :active="request()->routeIs('violations.create')">Input Pelanggaran</x-nav-item>
|
<x-nav-item href="{{ route('violations.create') }}" icon="plus-circle" :active="request()->routeIs('violations.create')">Input Pelanggaran</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('view-violations')
|
||||||
<x-nav-item href="{{ route('violations.index') }}" icon="exclamation-triangle" :active="request()->routeIs('violations.index')">Data Pelanggaran</x-nav-item>
|
<x-nav-item href="{{ route('violations.index') }}" icon="exclamation-triangle" :active="request()->routeIs('violations.index')">Data Pelanggaran</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('view-students')
|
||||||
<x-nav-item href="{{ route('students.index') }}" icon="users" :active="request()->routeIs('students.index*')">Data Siswa</x-nav-item>
|
<x-nav-item href="{{ route('students.index') }}" icon="users" :active="request()->routeIs('students.index*')">Data Siswa</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('manage-attendance')
|
||||||
<x-nav-item href="{{ route('attendances.index') }}" icon="clipboard-check" :active="request()->routeIs('attendances*')">Presensi Siswa</x-nav-item>
|
<x-nav-item href="{{ route('attendances.index') }}" icon="clipboard-check" :active="request()->routeIs('attendances*')">Presensi Siswa</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('view-sp-letters')
|
||||||
<x-nav-item href="{{ route('sp-letters.index') }}" icon="document-text" :active="request()->routeIs('sp-letters.*')">Surat Peringatan</x-nav-item>
|
<x-nav-item href="{{ route('sp-letters.index') }}" icon="document-text" :active="request()->routeIs('sp-letters.*')">Surat Peringatan</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
|
||||||
|
@canPermission('categories-manage')
|
||||||
<div class="pt-4 mt-4 border-t border-gray-200">
|
<div class="pt-4 mt-4 border-t border-gray-200">
|
||||||
<p class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Pengaturan</p>
|
<p class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Master Data</p>
|
||||||
</div>
|
</div>
|
||||||
<x-nav-item href="{{ route('settings.categories') }}" icon="tag" :active="request()->routeIs('settings.categories*')">Kategori Pelanggaran</x-nav-item>
|
<x-nav-item href="{{ route('settings.categories') }}" icon="tag" :active="request()->routeIs('settings.categories*')">Kategori Pelanggaran</x-nav-item>
|
||||||
<x-nav-item href="{{ route('settings.violation-types') }}" icon="list" :active="request()->routeIs('settings.violation-types*')">Jenis Pelanggaran</x-nav-item>
|
<x-nav-item href="{{ route('settings.violation-types') }}" icon="list" :active="request()->routeIs('settings.violation-types*')">Jenis Pelanggaran</x-nav-item>
|
||||||
<x-nav-item href="{{ route('settings.thresholds') }}" icon="chart-bar" :active="request()->routeIs('settings.thresholds*')">Ambang SP</x-nav-item>
|
<x-nav-item href="{{ route('settings.thresholds') }}" icon="chart-bar" :active="request()->routeIs('settings.thresholds*')">Ambang SP</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
|
||||||
|
@canPermission('settings-manage')
|
||||||
|
<div class="pt-4 mt-4 border-t border-gray-200">
|
||||||
|
<p class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Administrasi</p>
|
||||||
|
</div>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('sync-data')
|
||||||
<x-nav-item href="{{ route('settings.sync') }}" icon="refresh" :active="request()->routeIs('settings.sync*')">Sinkronisasi</x-nav-item>
|
<x-nav-item href="{{ route('settings.sync') }}" icon="refresh" :active="request()->routeIs('settings.sync*')">Sinkronisasi</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('users-manage')
|
||||||
<x-nav-item href="{{ route('users.index') }}" icon="users-cog" :active="request()->routeIs('users.*')">Manajemen User</x-nav-item>
|
<x-nav-item href="{{ route('users.index') }}" icon="users-cog" :active="request()->routeIs('users.*')">Manajemen User</x-nav-item>
|
||||||
|
<x-nav-item href="{{ route('settings.permissions') }}" icon="lock" :active="request()->routeIs('settings.permissions*')">Hak Akses Role</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('backup-database')
|
||||||
<x-nav-item href="{{ route('settings.backup') }}" icon="database" :active="request()->routeIs('settings.backup*')">Backup Database</x-nav-item>
|
<x-nav-item href="{{ route('settings.backup') }}" icon="database" :active="request()->routeIs('settings.backup*')">Backup Database</x-nav-item>
|
||||||
<x-nav-item href="{{ route('settings.reset') }}" icon="refresh" :active="request()->routeIs('settings.reset*')">Reset Aplikasi</x-nav-item>
|
@endcanPermission
|
||||||
|
@canPermission('reset-application')
|
||||||
|
<x-nav-item href="{{ route('settings.reset') }}" icon="arrows-rotate" :active="request()->routeIs('settings.reset*')">Reset Aplikasi</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
@canPermission('settings-manage')
|
||||||
<x-nav-item href="{{ route('settings.index') }}" icon="cog" :active="request()->routeIs('settings.index')">Pengaturan</x-nav-item>
|
<x-nav-item href="{{ route('settings.index') }}" icon="cog" :active="request()->routeIs('settings.index')">Pengaturan</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{{-- Footer --}}
|
{{-- Footer --}}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Hak Akses Role')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Hak Akses Role</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Atur permission/fitur apa saja yang bisa diakses oleh setiap role pengguna.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (session('success'))
|
||||||
|
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-sm text-green-700">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('settings.permissions.update') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="overflow-x-auto bg-white rounded-xl shadow-sm border border-gray-200">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50 border-b border-gray-200">
|
||||||
|
<th class="text-left px-4 py-3 font-semibold text-gray-700">Permission</th>
|
||||||
|
@foreach ($roles as $role)
|
||||||
|
<th class="text-center px-3 py-3 font-semibold text-gray-700 capitalize">{{ $role }}</th>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($permissions as $group => $groupPermissions)
|
||||||
|
<tr class="bg-gray-100 border-b border-gray-200">
|
||||||
|
<td colspan="{{ count($roles) + 1 }}" class="px-4 py-2 font-bold text-gray-700 uppercase text-xs tracking-wider">
|
||||||
|
{{ $group === 'master-data' ? 'Master Data' : ($group === 'administrasi' ? 'Administrasi' : $group) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@foreach ($groupPermissions as $perm)
|
||||||
|
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="font-medium text-gray-800">{{ $perm->label }}</div>
|
||||||
|
<div class="text-xs text-gray-400 font-mono">{{ $perm->key }}</div>
|
||||||
|
</td>
|
||||||
|
@foreach ($roles as $role)
|
||||||
|
@php
|
||||||
|
$checked = in_array($perm->id, $rolePermissions[$role] ?? []);
|
||||||
|
$disabled = ($role === 'admin');
|
||||||
|
@endphp
|
||||||
|
<td class="text-center px-3 py-3">
|
||||||
|
<input type="checkbox"
|
||||||
|
name="permissions[{{ $role }}][]"
|
||||||
|
value="{{ $perm->id }}"
|
||||||
|
{{ $checked ? 'checked' : '' }}
|
||||||
|
{{ $disabled ? 'disabled' : '' }}
|
||||||
|
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 cursor-pointer">
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex items-center justify-between">
|
||||||
|
<p class="text-xs text-gray-400">Role <strong>Admin</strong> selalu memiliki semua hak akses (tidak bisa diubah).</p>
|
||||||
|
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-medium">
|
||||||
|
Simpan Hak Akses
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
+106
-71
@@ -22,92 +22,127 @@ Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
|
|||||||
Route::post('/login', [LoginController::class, 'login']);
|
Route::post('/login', [LoginController::class, 'login']);
|
||||||
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');
|
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');
|
||||||
|
|
||||||
// Protected routes
|
// ===== Protected Routes (auth required) =====
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
|
||||||
|
|
||||||
// Notifications
|
// ===== Umum (permission-based) =====
|
||||||
Route::get('/notifications/unread-count', [DashboardController::class, 'getUnreadCount'])->name('notifications.unread-count');
|
|
||||||
Route::get('/calendar/data', [DashboardController::class, 'getCalendarData'])->name('calendar.data');
|
|
||||||
Route::get('/attendances', [\App\Http\Controllers\AttendanceController::class, 'index'])->name('attendances.index');
|
|
||||||
Route::get('/attendances/create', [\App\Http\Controllers\AttendanceController::class, 'create'])->name('attendances.create');
|
|
||||||
Route::post('/attendances', [\App\Http\Controllers\AttendanceController::class, 'store'])->name('attendances.store');
|
|
||||||
Route::get('/attendances/recap', [\App\Http\Controllers\AttendanceController::class, 'recap'])->name('attendances.recap');
|
|
||||||
Route::get('/attendances/calendar-data', [\App\Http\Controllers\AttendanceController::class, 'calendarData'])->name('attendances.calendar-data');
|
|
||||||
|
|
||||||
Route::get('/notifications/recent', [DashboardController::class, 'getRecentNotifications'])->name('notifications.recent');
|
Route::middleware('permission:access-dashboard')->group(function () {
|
||||||
Route::post('/notifications/{id}/read', [DashboardController::class, 'markNotificationRead'])->name('notifications.read');
|
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||||
Route::post('/notifications/read-all', [DashboardController::class, 'markAllNotificationsRead'])->name('notifications.read-all');
|
Route::get('/calendar/data', [DashboardController::class, 'getCalendarData'])->name('calendar.data');
|
||||||
Route::get('/notifications', [DashboardController::class, 'notificationsIndex'])->name('notifications.index');
|
});
|
||||||
|
|
||||||
// Violations
|
Route::middleware('permission:view-notifications')->group(function () {
|
||||||
Route::get('/violations', [ViolationController::class, 'index'])->name('violations.index');
|
Route::get('/notifications/unread-count', [DashboardController::class, 'getUnreadCount'])->name('notifications.unread-count');
|
||||||
Route::get('/violations/create', [ViolationController::class, 'create'])->name('violations.create');
|
Route::get('/notifications/recent', [DashboardController::class, 'getRecentNotifications'])->name('notifications.recent');
|
||||||
Route::post('/violations', [ViolationController::class, 'store'])->name('violations.store');
|
Route::post('/notifications/{id}/read', [DashboardController::class, 'markNotificationRead'])->name('notifications.read');
|
||||||
Route::get('/violations/{violation}', [ViolationController::class, 'show'])->name('violations.show');
|
Route::post('/notifications/read-all', [DashboardController::class, 'markAllNotificationsRead'])->name('notifications.read-all');
|
||||||
Route::post('/violations/{violation}/verify', [ViolationController::class, 'verify'])->name('violations.verify');
|
Route::get('/notifications', [DashboardController::class, 'notificationsIndex'])->name('notifications.index');
|
||||||
Route::delete('/violations/{violation}', [ViolationController::class, 'destroy'])->name('violations.destroy');
|
});
|
||||||
Route::post('/violations/{violation}/handling', [ViolationController::class, 'storeHandling'])->name('violations.handling.store');
|
|
||||||
Route::post('/violations/{violation}/resolve', [ViolationController::class, 'resolveHandling'])->name('violations.resolve');
|
|
||||||
Route::delete('/violations/{violation}/handling/{handling}', [ViolationController::class, 'destroyHandling'])->name('violations.handling.destroy');
|
|
||||||
Route::get('/api/students/search', [ViolationController::class, 'searchStudents'])->name('api.students.search');
|
|
||||||
|
|
||||||
// Students
|
Route::middleware('permission:input-violations')->group(function () {
|
||||||
Route::get('/students', [StudentReportController::class, 'index'])->name('students.index');
|
Route::get('/violations/create', [ViolationController::class, 'create'])->name('violations.create');
|
||||||
Route::get('/students/{student}', [StudentReportController::class, 'show'])->name('students.show');
|
Route::post('/violations', [ViolationController::class, 'store'])->name('violations.store');
|
||||||
Route::put('/students/{student}', [StudentReportController::class, 'update'])->name('students.update');
|
});
|
||||||
|
|
||||||
// SP Letters
|
Route::middleware('permission:view-violations')->group(function () {
|
||||||
Route::get('/sp-letters', [SpLetterController::class, 'index'])->name('sp-letters.index');
|
Route::get('/violations', [ViolationController::class, 'index'])->name('violations.index');
|
||||||
Route::get('/sp-letters/{spLetter}', [SpLetterController::class, 'show'])->name('sp-letters.show');
|
Route::get('/violations/{violation}', [ViolationController::class, 'show'])->name('violations.show');
|
||||||
Route::get('/sp-letters/{spLetter}/print', [SpLetterController::class, 'print'])->name('sp-letters.print');
|
Route::post('/violations/{violation}/verify', [ViolationController::class, 'verify'])->name('violations.verify');
|
||||||
|
Route::delete('/violations/{violation}', [ViolationController::class, 'destroy'])->name('violations.destroy');
|
||||||
|
Route::post('/violations/{violation}/handling', [ViolationController::class, 'storeHandling'])->name('violations.handling.store');
|
||||||
|
Route::post('/violations/{violation}/resolve', [ViolationController::class, 'resolveHandling'])->name('violations.resolve');
|
||||||
|
Route::delete('/violations/{violation}/handling/{handling}', [ViolationController::class, 'destroyHandling'])->name('violations.handling.destroy');
|
||||||
|
Route::get('/api/students/search', [ViolationController::class, 'searchStudents'])->name('api.students.search');
|
||||||
|
});
|
||||||
|
|
||||||
// Settings
|
Route::middleware('permission:view-students')->group(function () {
|
||||||
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
Route::get('/students', [StudentReportController::class, 'index'])->name('students.index');
|
||||||
Route::post('/settings', [SettingsController::class, 'update'])->name('settings.update');
|
Route::get('/students/{student}', [StudentReportController::class, 'show'])->name('students.show');
|
||||||
|
Route::put('/students/{student}', [StudentReportController::class, 'update'])->name('students.update');
|
||||||
|
});
|
||||||
|
|
||||||
// Master Data
|
Route::middleware('permission:manage-attendance')->group(function () {
|
||||||
Route::get('/settings/categories', [MasterDataController::class, 'categories'])->name('settings.categories');
|
Route::get('/attendances', [\App\Http\Controllers\AttendanceController::class, 'index'])->name('attendances.index');
|
||||||
Route::post('/settings/categories', [MasterDataController::class, 'storeCategory'])->name('settings.categories.store');
|
Route::get('/attendances/create', [\App\Http\Controllers\AttendanceController::class, 'create'])->name('attendances.create');
|
||||||
Route::put('/settings/categories/{category}', [MasterDataController::class, 'updateCategory'])->name('settings.categories.update');
|
Route::post('/attendances', [\App\Http\Controllers\AttendanceController::class, 'store'])->name('attendances.store');
|
||||||
Route::delete('/settings/categories/{category}', [MasterDataController::class, 'destroyCategory'])->name('settings.categories.destroy');
|
Route::get('/attendances/recap', [\App\Http\Controllers\AttendanceController::class, 'recap'])->name('attendances.recap');
|
||||||
|
Route::get('/attendances/calendar-data', [\App\Http\Controllers\AttendanceController::class, 'calendarData'])->name('attendances.calendar-data');
|
||||||
|
});
|
||||||
|
|
||||||
Route::get('/settings/violation-types', [MasterDataController::class, 'types'])->name('settings.violation-types');
|
Route::middleware('permission:view-sp-letters')->group(function () {
|
||||||
Route::post('/settings/violation-types', [MasterDataController::class, 'storeType'])->name('settings.violation-types.store');
|
Route::get('/sp-letters', [SpLetterController::class, 'index'])->name('sp-letters.index');
|
||||||
Route::put('/settings/violation-types/{type}', [MasterDataController::class, 'updateType'])->name('settings.violation-types.update');
|
Route::get('/sp-letters/{spLetter}', [SpLetterController::class, 'show'])->name('sp-letters.show');
|
||||||
Route::delete('/settings/violation-types/{type}', [MasterDataController::class, 'destroyType'])->name('settings.violation-types.destroy');
|
Route::get('/sp-letters/{spLetter}/print', [SpLetterController::class, 'print'])->name('sp-letters.print');
|
||||||
|
});
|
||||||
|
|
||||||
// Import
|
// ===== Master Data (permission-based) =====
|
||||||
Route::get('/settings/import/template', [ImportController::class, 'downloadTemplate'])->name('settings.import.template');
|
Route::middleware('permission:categories-manage')->group(function () {
|
||||||
Route::get('/settings/export/violation-types', [ImportController::class, 'exportViolationTypes'])->name('settings.export.violation-types');
|
Route::get('/settings/categories', [MasterDataController::class, 'categories'])->name('settings.categories');
|
||||||
Route::get('/settings/backup', [\App\Http\Controllers\Admin\BackupController::class, 'index'])->name('settings.backup');
|
Route::post('/settings/categories', [MasterDataController::class, 'storeCategory'])->name('settings.categories.store');
|
||||||
Route::post('/settings/backup/create', [\App\Http\Controllers\Admin\BackupController::class, 'create'])->name('settings.backup.create');
|
Route::put('/settings/categories/{category}', [MasterDataController::class, 'updateCategory'])->name('settings.categories.update');
|
||||||
Route::get('/settings/backup/download/{filename}', [\App\Http\Controllers\Admin\BackupController::class, 'download'])->name('settings.backup.download');
|
Route::delete('/settings/categories/{category}', [MasterDataController::class, 'destroyCategory'])->name('settings.categories.destroy');
|
||||||
Route::delete('/settings/backup/{filename}', [\App\Http\Controllers\Admin\BackupController::class, 'destroy'])->name('settings.backup.destroy');
|
});
|
||||||
Route::post('/settings/backup/restore', [\App\Http\Controllers\Admin\BackupController::class, 'restore'])->name('settings.backup.restore');
|
|
||||||
Route::get('/export/violations', [\App\Http\Controllers\Admin\ViolationExportController::class, 'export'])->name('violations.export');
|
|
||||||
Route::post('/settings/import/violation-types', [ImportController::class, 'importViolationTypes'])->name('settings.import.violation-types');
|
|
||||||
|
|
||||||
Route::get('/settings/thresholds', [MasterDataController::class, 'thresholds'])->name('settings.thresholds');
|
Route::middleware('permission:violation-types-manage')->group(function () {
|
||||||
Route::put('/settings/thresholds', [MasterDataController::class, 'updateThresholds'])->name('settings.thresholds.update');
|
Route::get('/settings/violation-types', [MasterDataController::class, 'types'])->name('settings.violation-types');
|
||||||
Route::post('/settings/thresholds', [MasterDataController::class, 'storeThreshold'])->name('settings.thresholds.store');
|
Route::post('/settings/violation-types', [MasterDataController::class, 'storeType'])->name('settings.violation-types.store');
|
||||||
Route::delete('/settings/thresholds/{threshold}', [MasterDataController::class, 'destroyThreshold'])->name('settings.thresholds.destroy');
|
Route::put('/settings/violation-types/{type}', [MasterDataController::class, 'updateType'])->name('settings.violation-types.update');
|
||||||
|
Route::delete('/settings/violation-types/{type}', [MasterDataController::class, 'destroyType'])->name('settings.violation-types.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
// Sync
|
Route::middleware('permission:thresholds-manage')->group(function () {
|
||||||
Route::get('/settings/sync', [SyncController::class, 'index'])->name('settings.sync');
|
Route::get('/settings/thresholds', [MasterDataController::class, 'thresholds'])->name('settings.thresholds');
|
||||||
Route::post('/settings/sync', [SyncController::class, 'syncNow'])->name('settings.sync.run');
|
Route::put('/settings/thresholds', [MasterDataController::class, 'updateThresholds'])->name('settings.thresholds.update');
|
||||||
|
Route::post('/settings/thresholds', [MasterDataController::class, 'storeThreshold'])->name('settings.thresholds.store');
|
||||||
|
Route::delete('/settings/thresholds/{threshold}', [MasterDataController::class, 'destroyThreshold'])->name('settings.thresholds.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
// User Management
|
Route::middleware('permission:violations-export')->group(function () {
|
||||||
Route::get('/users', [UserController::class, 'index'])->name('users.index');
|
Route::get('/export/violations', [\App\Http\Controllers\Admin\ViolationExportController::class, 'export'])->name('violations.export');
|
||||||
Route::post('/users', [UserController::class, 'store'])->name('users.store');
|
});
|
||||||
Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update');
|
|
||||||
Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy');
|
|
||||||
|
|
||||||
// Reset Application
|
// ===== Administrasi (permission-based) =====
|
||||||
Route::get('/settings/reset', [App\Http\Controllers\Admin\ResetController::class, 'index'])->name('settings.reset');
|
Route::middleware('permission:settings-manage')->group(function () {
|
||||||
Route::post('/settings/reset', [App\Http\Controllers\Admin\ResetController::class, 'reset'])->name('settings.reset.run');
|
Route::get('/settings', [SettingsController::class, 'index'])->name('settings.index');
|
||||||
|
Route::post('/settings', [SettingsController::class, 'update'])->name('settings.update');
|
||||||
|
});
|
||||||
|
|
||||||
// Profile Settings
|
Route::middleware('permission:import-data')->group(function () {
|
||||||
|
Route::get('/settings/import/template', [ImportController::class, 'downloadTemplate'])->name('settings.import.template');
|
||||||
|
Route::get('/settings/export/violation-types', [ImportController::class, 'exportViolationTypes'])->name('settings.export.violation-types');
|
||||||
|
Route::post('/settings/import/violation-types', [ImportController::class, 'importViolationTypes'])->name('settings.import.violation-types');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('permission:backup-database')->group(function () {
|
||||||
|
Route::get('/settings/backup', [\App\Http\Controllers\Admin\BackupController::class, 'index'])->name('settings.backup');
|
||||||
|
Route::post('/settings/backup/create', [\App\Http\Controllers\Admin\BackupController::class, 'create'])->name('settings.backup.create');
|
||||||
|
Route::get('/settings/backup/download/{filename}', [\App\Http\Controllers\Admin\BackupController::class, 'download'])->name('settings.backup.download');
|
||||||
|
Route::delete('/settings/backup/{filename}', [\App\Http\Controllers\Admin\BackupController::class, 'destroy'])->name('settings.backup.destroy');
|
||||||
|
Route::post('/settings/backup/restore', [\App\Http\Controllers\Admin\BackupController::class, 'restore'])->name('settings.backup.restore');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('permission:sync-data')->group(function () {
|
||||||
|
Route::get('/settings/sync', [SyncController::class, 'index'])->name('settings.sync');
|
||||||
|
Route::post('/settings/sync', [SyncController::class, 'syncNow'])->name('settings.sync.run');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('permission:users-manage')->group(function () {
|
||||||
|
Route::get('/users', [UserController::class, 'index'])->name('users.index');
|
||||||
|
Route::post('/users', [UserController::class, 'store'])->name('users.store');
|
||||||
|
Route::put('/users/{user}', [UserController::class, 'update'])->name('users.update');
|
||||||
|
Route::delete('/users/{user}', [UserController::class, 'destroy'])->name('users.destroy');
|
||||||
|
|
||||||
|
Route::get('/settings/permissions', [\App\Http\Controllers\Admin\PermissionController::class, 'index'])->name('settings.permissions');
|
||||||
|
Route::post('/settings/permissions', [\App\Http\Controllers\Admin\PermissionController::class, 'update'])->name('settings.permissions.update');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('permission:reset-application')->group(function () {
|
||||||
|
Route::get('/settings/reset', [\App\Http\Controllers\Admin\ResetController::class, 'index'])->name('settings.reset');
|
||||||
|
Route::post('/settings/reset', [\App\Http\Controllers\Admin\ResetController::class, 'reset'])->name('settings.reset.run');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Profile (all roles — no permission needed, manage own account)
|
||||||
Route::get('/profile', [ProfileController::class, 'index'])->name('profile.index');
|
Route::get('/profile', [ProfileController::class, 'index'])->name('profile.index');
|
||||||
Route::post('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::post('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user