From 9c741f13f77c7d6d38bae1252e5891552c42eff3 Mon Sep 17 00:00:00 2001 From: Novy San87 Date: Thu, 23 Jul 2026 06:49:28 +0700 Subject: [PATCH] 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 --- .../Admin/PermissionController.php | 79 ++++++++ app/Http/Middleware/CheckRole.php | 32 ++++ app/Http/Middleware/PermissionMiddleware.php | 47 +++++ app/Models/User.php | 17 ++ app/Providers/AppServiceProvider.php | 21 +++ bootstrap/app.php | 5 +- ...07_23_063835_create_permissions_tables.php | 89 +++++++++ resources/views/components/nav-item.blade.php | 2 + resources/views/layouts/app.blade.php | 35 +++- .../views/settings/permissions.blade.php | 71 +++++++ routes/web.php | 177 +++++++++++------- 11 files changed, 501 insertions(+), 74 deletions(-) create mode 100644 app/Http/Controllers/Admin/PermissionController.php create mode 100644 app/Http/Middleware/CheckRole.php create mode 100644 app/Http/Middleware/PermissionMiddleware.php create mode 100644 database/migrations/2026_07_23_063835_create_permissions_tables.php create mode 100644 resources/views/settings/permissions.blade.php diff --git a/app/Http/Controllers/Admin/PermissionController.php b/app/Http/Controllers/Admin/PermissionController.php new file mode 100644 index 0000000..3b30ad4 --- /dev/null +++ b/app/Http/Controllers/Admin/PermissionController.php @@ -0,0 +1,79 @@ +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.'); + } +} diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php new file mode 100644 index 0000000..31f1328 --- /dev/null +++ b/app/Http/Middleware/CheckRole.php @@ -0,0 +1,32 @@ +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.'); + } +} diff --git a/app/Http/Middleware/PermissionMiddleware.php b/app/Http/Middleware/PermissionMiddleware.php new file mode 100644 index 0000000..59e8bc3 --- /dev/null +++ b/app/Http/Middleware/PermissionMiddleware.php @@ -0,0 +1,47 @@ +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.'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index a8f250a..704ce6c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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() { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7ad9677..ae22a4c 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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); + }); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 7a2848f..af2176c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -12,7 +12,10 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->alias([ + 'role' => \App\Http\Middleware\CheckRole::class, + 'permission' => \App\Http\Middleware\PermissionMiddleware::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/database/migrations/2026_07_23_063835_create_permissions_tables.php b/database/migrations/2026_07_23_063835_create_permissions_tables.php new file mode 100644 index 0000000..d153a99 --- /dev/null +++ b/database/migrations/2026_07_23_063835_create_permissions_tables.php @@ -0,0 +1,89 @@ +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'); + } +}; diff --git a/resources/views/components/nav-item.blade.php b/resources/views/components/nav-item.blade.php index 2f0dd2f..c8ad86a 100644 --- a/resources/views/components/nav-item.blade.php +++ b/resources/views/components/nav-item.blade.php @@ -20,6 +20,8 @@ $faIcons = [ 'cog' => 'fa-solid fa-gear', 'users-cog' => 'fa-solid fa-user-gear', 'database' => 'fa-solid fa-database', + 'arrows-rotate' => 'fa-solid fa-arrows-rotate', + 'lock' => 'fa-solid fa-lock', ]; $faClass = $faIcons[$icon] ?? 'fa-solid fa-circle'; @endphp diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 1c90f39..0fb8829 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -44,24 +44,55 @@ {{-- Footer --}} diff --git a/resources/views/settings/permissions.blade.php b/resources/views/settings/permissions.blade.php new file mode 100644 index 0000000..4d40ffd --- /dev/null +++ b/resources/views/settings/permissions.blade.php @@ -0,0 +1,71 @@ +@extends('layouts.app') + +@section('title', 'Hak Akses Role') + +@section('content') +
+
+

Hak Akses Role

+

Atur permission/fitur apa saja yang bisa diakses oleh setiap role pengguna.

+
+ + @if (session('success')) +
{{ session('success') }}
+ @endif + +
+ @csrf + +
+ + + + + @foreach ($roles as $role) + + @endforeach + + + + @foreach ($permissions as $group => $groupPermissions) + + + + @foreach ($groupPermissions as $perm) + + + @foreach ($roles as $role) + @php + $checked = in_array($perm->id, $rolePermissions[$role] ?? []); + $disabled = ($role === 'admin'); + @endphp + + @endforeach + + @endforeach + @endforeach + +
Permission{{ $role }}
+ {{ $group === 'master-data' ? 'Master Data' : ($group === 'administrasi' ? 'Administrasi' : $group) }} +
+
{{ $perm->label }}
+
{{ $perm->key }}
+
+ +
+
+ +
+

Role Admin selalu memiliki semua hak akses (tidak bisa diubah).

+ +
+
+
+@endsection diff --git a/routes/web.php b/routes/web.php index 7e4e4a2..2fc8259 100644 --- a/routes/web.php +++ b/routes/web.php @@ -22,92 +22,127 @@ Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login'); Route::post('/login', [LoginController::class, 'login']); Route::post('/logout', [LoginController::class, 'logout'])->name('logout'); -// Protected routes +// ===== Protected Routes (auth required) ===== Route::middleware('auth')->group(function () { - Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); - // Notifications - 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'); + // ===== Umum (permission-based) ===== - Route::get('/notifications/recent', [DashboardController::class, 'getRecentNotifications'])->name('notifications.recent'); - Route::post('/notifications/{id}/read', [DashboardController::class, 'markNotificationRead'])->name('notifications.read'); - Route::post('/notifications/read-all', [DashboardController::class, 'markAllNotificationsRead'])->name('notifications.read-all'); - Route::get('/notifications', [DashboardController::class, 'notificationsIndex'])->name('notifications.index'); + Route::middleware('permission:access-dashboard')->group(function () { + Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); + Route::get('/calendar/data', [DashboardController::class, 'getCalendarData'])->name('calendar.data'); + }); - // Violations - Route::get('/violations', [ViolationController::class, 'index'])->name('violations.index'); - Route::get('/violations/create', [ViolationController::class, 'create'])->name('violations.create'); - Route::post('/violations', [ViolationController::class, 'store'])->name('violations.store'); - Route::get('/violations/{violation}', [ViolationController::class, 'show'])->name('violations.show'); - 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'); + Route::middleware('permission:view-notifications')->group(function () { + Route::get('/notifications/unread-count', [DashboardController::class, 'getUnreadCount'])->name('notifications.unread-count'); + Route::get('/notifications/recent', [DashboardController::class, 'getRecentNotifications'])->name('notifications.recent'); + Route::post('/notifications/{id}/read', [DashboardController::class, 'markNotificationRead'])->name('notifications.read'); + Route::post('/notifications/read-all', [DashboardController::class, 'markAllNotificationsRead'])->name('notifications.read-all'); + Route::get('/notifications', [DashboardController::class, 'notificationsIndex'])->name('notifications.index'); + }); - // Students - Route::get('/students', [StudentReportController::class, 'index'])->name('students.index'); - Route::get('/students/{student}', [StudentReportController::class, 'show'])->name('students.show'); - Route::put('/students/{student}', [StudentReportController::class, 'update'])->name('students.update'); + Route::middleware('permission:input-violations')->group(function () { + Route::get('/violations/create', [ViolationController::class, 'create'])->name('violations.create'); + Route::post('/violations', [ViolationController::class, 'store'])->name('violations.store'); + }); - // SP Letters - Route::get('/sp-letters', [SpLetterController::class, 'index'])->name('sp-letters.index'); - Route::get('/sp-letters/{spLetter}', [SpLetterController::class, 'show'])->name('sp-letters.show'); - Route::get('/sp-letters/{spLetter}/print', [SpLetterController::class, 'print'])->name('sp-letters.print'); + Route::middleware('permission:view-violations')->group(function () { + Route::get('/violations', [ViolationController::class, 'index'])->name('violations.index'); + Route::get('/violations/{violation}', [ViolationController::class, 'show'])->name('violations.show'); + 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::get('/settings', [SettingsController::class, 'index'])->name('settings.index'); - Route::post('/settings', [SettingsController::class, 'update'])->name('settings.update'); + Route::middleware('permission:view-students')->group(function () { + Route::get('/students', [StudentReportController::class, 'index'])->name('students.index'); + Route::get('/students/{student}', [StudentReportController::class, 'show'])->name('students.show'); + Route::put('/students/{student}', [StudentReportController::class, 'update'])->name('students.update'); + }); - // Master Data - Route::get('/settings/categories', [MasterDataController::class, 'categories'])->name('settings.categories'); - Route::post('/settings/categories', [MasterDataController::class, 'storeCategory'])->name('settings.categories.store'); - Route::put('/settings/categories/{category}', [MasterDataController::class, 'updateCategory'])->name('settings.categories.update'); - Route::delete('/settings/categories/{category}', [MasterDataController::class, 'destroyCategory'])->name('settings.categories.destroy'); + Route::middleware('permission:manage-attendance')->group(function () { + 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('/settings/violation-types', [MasterDataController::class, 'types'])->name('settings.violation-types'); - Route::post('/settings/violation-types', [MasterDataController::class, 'storeType'])->name('settings.violation-types.store'); - 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'); + Route::middleware('permission:view-sp-letters')->group(function () { + Route::get('/sp-letters', [SpLetterController::class, 'index'])->name('sp-letters.index'); + Route::get('/sp-letters/{spLetter}', [SpLetterController::class, 'show'])->name('sp-letters.show'); + Route::get('/sp-letters/{spLetter}/print', [SpLetterController::class, 'print'])->name('sp-letters.print'); + }); - // Import - 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::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::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'); + // ===== Master Data (permission-based) ===== + Route::middleware('permission:categories-manage')->group(function () { + Route::get('/settings/categories', [MasterDataController::class, 'categories'])->name('settings.categories'); + Route::post('/settings/categories', [MasterDataController::class, 'storeCategory'])->name('settings.categories.store'); + Route::put('/settings/categories/{category}', [MasterDataController::class, 'updateCategory'])->name('settings.categories.update'); + Route::delete('/settings/categories/{category}', [MasterDataController::class, 'destroyCategory'])->name('settings.categories.destroy'); + }); - Route::get('/settings/thresholds', [MasterDataController::class, 'thresholds'])->name('settings.thresholds'); - 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'); + Route::middleware('permission:violation-types-manage')->group(function () { + Route::get('/settings/violation-types', [MasterDataController::class, 'types'])->name('settings.violation-types'); + Route::post('/settings/violation-types', [MasterDataController::class, 'storeType'])->name('settings.violation-types.store'); + 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::get('/settings/sync', [SyncController::class, 'index'])->name('settings.sync'); - Route::post('/settings/sync', [SyncController::class, 'syncNow'])->name('settings.sync.run'); + Route::middleware('permission:thresholds-manage')->group(function () { + Route::get('/settings/thresholds', [MasterDataController::class, 'thresholds'])->name('settings.thresholds'); + 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::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::middleware('permission:violations-export')->group(function () { + Route::get('/export/violations', [\App\Http\Controllers\Admin\ViolationExportController::class, 'export'])->name('violations.export'); + }); - // Reset Application - 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'); + // ===== Administrasi (permission-based) ===== + Route::middleware('permission:settings-manage')->group(function () { + 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::post('/profile', [ProfileController::class, 'update'])->name('profile.update'); });