diff --git a/.gitignore b/.gitignore
index 8b0d909..80863cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,5 @@ yarn-error.log
/.nova
/.vscode
/.zed
+.kiro
+.github
\ No newline at end of file
diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php
new file mode 100644
index 0000000..beb154f
--- /dev/null
+++ b/app/Http/Controllers/Admin/UserController.php
@@ -0,0 +1,150 @@
+paginate(10);
+
+ return view('admin.users.index', compact('users'));
+ }
+
+ /**
+ * Show the form for creating a new user
+ */
+ public function create()
+ {
+ $roles = Role::all();
+
+ return view('admin.users.create', compact('roles'));
+ }
+
+ /**
+ * Store a newly created user
+ */
+ public function store(Request $request)
+ {
+ $validated = $request->validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
+ 'username' => ['required', 'string', 'max:255', 'unique:users'],
+ 'password' => [
+ 'required',
+ 'string',
+ 'min:8',
+ 'confirmed',
+ 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/',
+ ],
+ 'roles' => ['array'],
+ 'roles.*' => ['exists:roles,name'],
+ ]);
+
+ $user = User::create([
+ 'name' => $validated['name'],
+ 'email' => $validated['email'],
+ 'username' => $validated['username'],
+ 'password' => Hash::make($validated['password']),
+ ]);
+
+ if (!empty($validated['roles'])) {
+ $user->assignRole($validated['roles']);
+ }
+
+ return redirect()->route('admin.users.index')
+ ->with('success', 'Pengguna berhasil ditambahkan.');
+ }
+
+ /**
+ * Display the specified user
+ */
+ public function show(User $user)
+ {
+ $user->load('roles', 'permissions');
+
+ return view('admin.users.show', compact('user'));
+ }
+
+ /**
+ * Show the form for editing the specified user
+ */
+ public function edit(User $user)
+ {
+ $roles = Role::all();
+ $userRoles = $user->roles->pluck('name')->toArray();
+
+ return view('admin.users.edit', compact('user', 'roles', 'userRoles'));
+ }
+
+ /**
+ * Update the specified user
+ */
+ public function update(Request $request, User $user)
+ {
+ $validated = $request->validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
+ 'username' => ['required', 'string', 'max:255', Rule::unique('users')->ignore($user->id)],
+ 'password' => [
+ 'nullable',
+ 'string',
+ 'min:8',
+ 'confirmed',
+ 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/',
+ ],
+ 'roles' => ['array'],
+ 'roles.*' => ['exists:roles,name'],
+ ]);
+
+ $user->update([
+ 'name' => $validated['name'],
+ 'email' => $validated['email'],
+ 'username' => $validated['username'],
+ ]);
+
+ // Update password only if provided
+ if (!empty($validated['password'])) {
+ $user->update([
+ 'password' => Hash::make($validated['password']),
+ ]);
+ }
+
+ // Sync roles
+ if (isset($validated['roles'])) {
+ $user->syncRoles($validated['roles']);
+ } else {
+ $user->syncRoles([]);
+ }
+
+ return redirect()->route('admin.users.index')
+ ->with('success', 'Pengguna berhasil diperbarui.');
+ }
+
+ /**
+ * Remove the specified user
+ */
+ public function destroy(User $user)
+ {
+ // Prevent deleting current user
+ if ($user->id === auth()->id()) {
+ return redirect()->route('admin.users.index')
+ ->with('error', 'Anda tidak dapat menghapus akun sendiri.');
+ }
+
+ $user->delete();
+
+ return redirect()->route('admin.users.index')
+ ->with('success', 'Pengguna berhasil dihapus.');
+ }
+}
diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php
index 292f195..73ac591 100644
--- a/app/Http/Controllers/AuthController.php
+++ b/app/Http/Controllers/AuthController.php
@@ -3,38 +3,44 @@
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
-use App\Http\Requests\Auth\LoginRequest;
+use App\Http\Requests\Auth\ApiLoginRequest;
use App\Models\User;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
- // =============== API (Sanctum Token) ===============
- public function login(LoginRequest $request)
+ /**
+ * Handle API login using Sanctum tokens
+ */
+ public function login(ApiLoginRequest $request)
{
$data = $request->validated();
$identifier = $data['identifier'];
$password = $data['password'];
- $device = $data['device_name'] ?? $request->header('User-Agent') ?? 'api';
+ $device = $data['device_name'] ?? $request->header('User-Agent') ?? 'api-client';
+
+ // Find user by email or username
+ $user = User::where('email', $identifier)
+ ->orWhere('username', $identifier)
+ ->first();
- $user = User::where('email', $identifier)->first();
- if (!$user) {
- $user = User::where('username', $identifier)->first();
- }
if (!$user || !Hash::check($password, $user->password)) {
throw ValidationException::withMessages([
- 'identifier' => ['email/username/password yang diberikan salah.'],
+ 'identifier' => ['Email/username atau password yang diberikan salah.'],
]);
}
+ // Delete existing tokens for this device to prevent token accumulation
$user->tokens()->where('name', $device)->delete();
+
+ // Create new token
$token = $user->createToken($device)->plainTextToken;
return response()->json([
'success' => true,
+ 'message' => 'Login berhasil',
'token' => $token,
'token_type' => 'Bearer',
'user' => [
@@ -42,65 +48,66 @@ class AuthController extends Controller
'name' => $user->name,
'email' => $user->email,
'username' => $user->username,
+ 'roles' => $user->getRoleNames(),
+ 'permissions' => $user->getAllPermissions()->pluck('name'),
],
]);
}
+ /**
+ * Get authenticated user information
+ */
public function me(Request $request)
{
+ $user = $request->user();
+
return response()->json([
'success' => true,
- 'user' => $request->user(),
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ 'username' => $user->username,
+ 'roles' => $user->getRoleNames(),
+ 'permissions' => $user->getAllPermissions()->pluck('name'),
+ ],
]);
}
+ /**
+ * Logout current API session (revoke current token)
+ */
public function logout(Request $request)
{
- $request->user()->currentAccessToken()->delete();
- return response()->json(['success' => true]);
+ $user = $request->user();
+
+ // Revoke current access token
+ if ($user && method_exists($user, 'currentAccessToken')) {
+ $token = $user->currentAccessToken();
+ if ($token) {
+ $token->delete();
+ }
+ }
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Logout berhasil'
+ ]);
}
+ /**
+ * Logout from all devices (revoke all tokens)
+ */
public function logoutAll(Request $request)
{
- $request->user()->tokens()->delete();
- return response()->json(['success' => true]);
- }
+ $user = $request->user();
- // =============== Web (Session Guard) ===============
- public function sessionLogin(LoginRequest $request)
- {
- $data = $request->validated();
- $identifier = $data['identifier'];
- $password = $data['password'];
+ // Revoke all tokens for this user
+ $user->tokens()->delete();
- $user = User::where('email', $identifier)->first();
- if (!$user) {
- $user = User::where('username', $identifier)->first();
- }
-
- if (!$user || !Hash::check($password, $user->password)) {
- throw ValidationException::withMessages([
- 'identifier' => ['email/username/password yang diberikan salah.'],
- ]);
- }
-
- Auth::login($user, true);
- $request->session()->regenerate();
-
- return response()->json(['success' => true]);
- }
-
- public function sessionLogout(Request $request)
- {
- Auth::logout();
- $request->session()->invalidate();
- $request->session()->regenerateToken();
-
- if ($request->expectsJson()) {
- return response()->json(['success' => true]);
- }
-
- return redirect()->route('login.index');
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Logout dari semua perangkat berhasil'
+ ]);
}
}
-
diff --git a/app/Http/Controllers/WebAuthController.php b/app/Http/Controllers/WebAuthController.php
index 5fe7300..df71765 100644
--- a/app/Http/Controllers/WebAuthController.php
+++ b/app/Http/Controllers/WebAuthController.php
@@ -2,49 +2,90 @@
namespace App\Http\Controllers;
+use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Models\User;
+use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Cookie;
+use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
-use Illuminate\Http\Request;
class WebAuthController extends Controller
{
+ /**
+ * Handle web-based login using session authentication
+ */
public function sessionLogin(LoginRequest $request)
{
$data = $request->validated();
$identifier = $data['identifier'];
$password = $data['password'];
- $user = User::where('email', $identifier)->first();
- if (!$user) {
- $user = User::where('username', $identifier)->first();
- }
+ // Find user by email or username
+ $user = User::where('email', $identifier)
+ ->orWhere('username', $identifier)
+ ->first();
if (!$user || !Hash::check($password, $user->password)) {
throw ValidationException::withMessages([
- 'identifier' => ['email/username/password yang diberikan salah.'],
+ 'identifier' => ['Email/username atau password yang diberikan salah.'],
]);
}
- Auth::login($user, true);
- request()->session()->regenerate();
+ // Login using session guard (creates session cookies)
+ Auth::guard('web')->login($user, false);
+ $request->session()->regenerate();
- return response()->json(['success' => true]);
+ if ($request->expectsJson()) {
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Login berhasil',
+ 'user' => [
+ 'id' => $user->id,
+ 'name' => $user->name,
+ 'email' => $user->email,
+ 'username' => $user->username,
+ ],
+ ]);
+ }
+
+ return redirect()->intended('/dashboard');
}
- public function logout(Request $request)
+
+ /**
+ * Handle web-based logout (clears session cookies)
+ */
+ public function sessionLogout(Request $request)
{
- Auth::logout();
+ $user = Auth::user();
+
+ // Logout from session
+ Auth::guard('web')->logout();
+
+ // Clear remember token if exists
+ if ($user) {
+ $user->setRememberToken(Str::random(60));
+ $user->save();
+ }
+
+ // Invalidate session and regenerate CSRF token
$request->session()->invalidate();
$request->session()->regenerateToken();
+ // Clear authentication cookies
+ Cookie::queue(Cookie::forget(Auth::getRecallerName()));
+ Cookie::queue(Cookie::forget(config('session.cookie')));
+ Cookie::queue(Cookie::forget('XSRF-TOKEN'));
+
if ($request->expectsJson()) {
- return response()->json(['success' => true]);
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Logout berhasil'
+ ]);
}
- return redirect()->route('login.index');
+ return redirect()->route('login.index')->with('message', 'Anda telah berhasil logout');
}
}
-
-
diff --git a/app/Http/Middleware/WebAuthMiddleware.php b/app/Http/Middleware/WebAuthMiddleware.php
new file mode 100644
index 0000000..c96d92b
--- /dev/null
+++ b/app/Http/Middleware/WebAuthMiddleware.php
@@ -0,0 +1,30 @@
+check()) {
+ if ($request->expectsJson()) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Unauthenticated'
+ ], 401);
+ }
+
+ return redirect()->route('login.index');
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Requests/Auth/ApiLoginRequest.php b/app/Http/Requests/Auth/ApiLoginRequest.php
new file mode 100644
index 0000000..d3ec4b4
--- /dev/null
+++ b/app/Http/Requests/Auth/ApiLoginRequest.php
@@ -0,0 +1,31 @@
+ ['required', 'string'],
+ 'password' => ['required', 'string'],
+ 'device_name' => ['nullable', 'string', 'max:255'],
+ ];
+ }
+
+ public function messages(): array
+ {
+ return [
+ 'identifier.required' => 'Email atau username wajib diisi.',
+ 'password.required' => 'Password wajib diisi.',
+ 'device_name.max' => 'Nama perangkat maksimal 255 karakter.',
+ ];
+ }
+}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 91776f4..67d3916 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -9,9 +9,9 @@ use Spatie\Permission\Middleware\RoleOrPermissionMiddleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
- web: __DIR__.'/../routes/web.php',
- api: __DIR__.'/../routes/api.php',
- commands: __DIR__.'/../routes/console.php',
+ web: __DIR__ . '/../routes/web.php',
+ api: __DIR__ . '/../routes/api.php',
+ commands: __DIR__ . '/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
@@ -20,6 +20,7 @@ return Application::configure(basePath: dirname(__DIR__))
'role' => RoleMiddleware::class,
'permission' => PermissionMiddleware::class,
'role_or_permission' => RoleOrPermissionMiddleware::class,
+ 'web.auth' => \App\Http\Middleware\WebAuthMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
diff --git a/config/auth.php b/config/auth.php
index 0ba5d5d..7faead5 100644
--- a/config/auth.php
+++ b/config/auth.php
@@ -40,6 +40,10 @@ return [
'driver' => 'session',
'provider' => 'users',
],
+ 'sanctum' => [
+ 'driver' => 'sanctum',
+ 'provider' => 'users',
+ ],
],
/*
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
index 584104c..9143838 100644
--- a/database/factories/UserFactory.php
+++ b/database/factories/UserFactory.php
@@ -26,6 +26,7 @@ class UserFactory extends Factory
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
+ 'username' => fake()->unique()->userName(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
@@ -37,7 +38,7 @@ class UserFactory extends Factory
*/
public function unverified(): static
{
- return $this->state(fn (array $attributes) => [
+ return $this->state(fn(array $attributes) => [
'email_verified_at' => null,
]);
}
diff --git a/database/seeders/RolesAndPermissionsSeeder.php b/database/seeders/RolesAndPermissionsSeeder.php
index d45240c..7bdc857 100644
--- a/database/seeders/RolesAndPermissionsSeeder.php
+++ b/database/seeders/RolesAndPermissionsSeeder.php
@@ -73,4 +73,3 @@ class RolesAndPermissionsSeeder extends Seeder
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
}
}
-
diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php
index 6f1d44b..e776cd9 100644
--- a/database/seeders/UserSeeder.php
+++ b/database/seeders/UserSeeder.php
@@ -11,7 +11,7 @@ class UserSeeder extends Seeder
public function run(): void
{
$user = User::updateOrCreate(
- ['email' => 'ammar@example.com'],
+ ['email' => 'ammar@dinaslhdki.id'],
[
'name' => 'Ammar',
'username' => 'ammar',
diff --git a/docs/authentication.md b/docs/authentication.md
new file mode 100644
index 0000000..7d56709
--- /dev/null
+++ b/docs/authentication.md
@@ -0,0 +1,222 @@
+# Authentication System Documentation
+
+## Overview
+
+The application uses a dual authentication system:
+
+- **Web Authentication**: Session-based with cookies for browser access
+- **API Authentication**: Token-based using Laravel Sanctum for API access
+
+## Web Authentication (Session + Cookies)
+
+### Login
+
+**Endpoint**: `POST /auth/session-login`
+**Controller**: `WebAuthController@sessionLogin`
+
+```bash
+curl -X POST http://localhost:8000/auth/session-login \
+ -H "Content-Type: application/json" \
+ -H "X-CSRF-TOKEN: your-csrf-token" \
+ -d '{
+ "identifier": "user@example.com",
+ "password": "password123"
+ }'
+```
+
+**Response**:
+
+```json
+{
+ "success": true,
+ "message": "Login berhasil",
+ "user": {
+ "id": 1,
+ "name": "John Doe",
+ "email": "user@example.com",
+ "username": "johndoe"
+ }
+}
+```
+
+### Logout
+
+**Endpoint**: `POST /auth/logout`
+**Controller**: `WebAuthController@sessionLogout`
+
+```bash
+curl -X POST http://localhost:8000/auth/logout \
+ -H "Content-Type: application/json" \
+ -H "X-CSRF-TOKEN: your-csrf-token"
+```
+
+### Protected Routes
+
+All admin routes are protected with `web.auth` middleware:
+
+- `/dashboard/*`
+- `/admin/*`
+
+## API Authentication (Token-based)
+
+### Login
+
+**Endpoint**: `POST /api/auth/login`
+**Controller**: `AuthController@login`
+
+```bash
+curl -X POST http://localhost:8000/api/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{
+ "identifier": "user@example.com",
+ "password": "password123",
+ "device_name": "mobile-app"
+ }'
+```
+
+**Response**:
+
+```json
+{
+ "success": true,
+ "message": "Login berhasil",
+ "token": "1|abc123def456...",
+ "token_type": "Bearer",
+ "user": {
+ "id": 1,
+ "name": "John Doe",
+ "email": "user@example.com",
+ "username": "johndoe",
+ "roles": ["admin"],
+ "permissions": ["dashboard.view", "settings.manage"]
+ }
+}
+```
+
+### Using API Token
+
+Include the token in the Authorization header:
+
+```bash
+curl -X GET http://localhost:8000/api/auth/me \
+ -H "Authorization: Bearer 1|abc123def456..."
+```
+
+### Get User Info
+
+**Endpoint**: `GET /api/auth/me`
+
+```bash
+curl -X GET http://localhost:8000/api/auth/me \
+ -H "Authorization: Bearer your-token"
+```
+
+### Logout (Current Device)
+
+**Endpoint**: `POST /api/auth/logout`
+
+```bash
+curl -X POST http://localhost:8000/api/auth/logout \
+ -H "Authorization: Bearer your-token"
+```
+
+### Logout All Devices
+
+**Endpoint**: `POST /api/auth/logout-all`
+
+```bash
+curl -X POST http://localhost:8000/api/auth/logout-all \
+ -H "Authorization: Bearer your-token"
+```
+
+## Configuration
+
+### Guards
+
+- `web`: Session-based authentication for web interface
+- `sanctum`: Token-based authentication for API
+
+### Middleware
+
+- `web.auth`: Custom middleware for web session authentication
+- `auth:sanctum`: Laravel Sanctum middleware for API authentication
+
+### Token Management
+
+- Tokens are device-specific (one token per device)
+- Old tokens for the same device are automatically revoked on new login
+- Tokens don't expire by default (configurable in sanctum config)
+
+## Security Features
+
+### Web Authentication
+
+- CSRF protection enabled
+- Session regeneration on login
+- Remember token invalidation on logout
+- Cookie cleanup on logout
+
+### API Authentication
+
+- Device-specific tokens
+- Token revocation on logout
+- Automatic cleanup of old tokens
+- Rate limiting (configurable)
+
+## Error Handling
+
+### Common Error Responses
+
+**Invalid Credentials**:
+
+```json
+{
+ "message": "The given data was invalid.",
+ "errors": {
+ "identifier": ["Email/username atau password yang diberikan salah."]
+ }
+}
+```
+
+**Unauthenticated**:
+
+```json
+{
+ "success": false,
+ "message": "Unauthenticated"
+}
+```
+
+## Testing
+
+### Web Authentication Test
+
+```bash
+# Login
+curl -c cookies.txt -X POST http://localhost:8000/auth/session-login \
+ -H "Content-Type: application/json" \
+ -d '{"identifier": "admin@example.com", "password": "password"}'
+
+# Access protected route
+curl -b cookies.txt http://localhost:8000/dashboard
+
+# Logout
+curl -b cookies.txt -X POST http://localhost:8000/auth/logout
+```
+
+### API Authentication Test
+
+```bash
+# Login and save token
+TOKEN=$(curl -X POST http://localhost:8000/api/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{"identifier": "admin@example.com", "password": "password"}' \
+ | jq -r '.token')
+
+# Use token
+curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/auth/me
+
+# Logout
+curl -X POST http://localhost:8000/api/auth/logout \
+ -H "Authorization: Bearer $TOKEN"
+```
diff --git a/docs/user-management.md b/docs/user-management.md
new file mode 100644
index 0000000..e817385
--- /dev/null
+++ b/docs/user-management.md
@@ -0,0 +1,221 @@
+# User Management System
+
+Sistem manajemen pengguna untuk aplikasi Perling dengan fitur CRUD lengkap dan integrasi role-based access control.
+
+## 🚀 Fitur
+
+### ✅ Manajemen Pengguna
+
+- **View (Index)**: Daftar semua pengguna dengan pagination
+- **Create**: Tambah pengguna baru dengan role assignment
+- **Show**: Detail lengkap pengguna termasuk role dan permissions
+- **Edit**: Update informasi pengguna dan role
+- **Delete**: Hapus pengguna (dengan proteksi untuk user yang sedang login)
+
+### 🔐 Keamanan
+
+- Password validation dengan requirements yang kuat
+- Role-based access control menggunakan Spatie Laravel Permission
+- Proteksi CSRF untuk semua form
+- Validasi unique untuk email dan username
+- Proteksi penghapusan akun sendiri
+
+## 📋 Endpoints
+
+| Method | Route | Action | Permission Required |
+| ------ | -------------------------- | -------------------- | ------------------- |
+| GET | `/admin/users` | Daftar pengguna | `settings.manage` |
+| GET | `/admin/users/create` | Form tambah pengguna | `settings.manage` |
+| POST | `/admin/users` | Simpan pengguna baru | `settings.manage` |
+| GET | `/admin/users/{user}` | Detail pengguna | `settings.manage` |
+| GET | `/admin/users/{user}/edit` | Form edit pengguna | `settings.manage` |
+| PUT | `/admin/users/{user}` | Update pengguna | `settings.manage` |
+| DELETE | `/admin/users/{user}` | Hapus pengguna | `settings.manage` |
+
+## 🎯 Validasi
+
+### Create User
+
+```php
+'name' => 'required|string|max:255',
+'email' => 'required|email|unique:users',
+'username' => 'required|string|unique:users',
+'password' => 'required|min:8|confirmed|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/',
+'roles' => 'array|exists:roles,name'
+```
+
+### Update User
+
+```php
+'name' => 'required|string|max:255',
+'email' => 'required|email|unique:users,email,{user_id}',
+'username' => 'required|string|unique:users,username,{user_id}',
+'password' => 'nullable|min:8|confirmed|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/',
+'roles' => 'array|exists:roles,name'
+```
+
+## 🗂️ File Structure
+
+```
+app/Http/Controllers/Admin/
+└── UserController.php # Controller utama
+
+resources/views/admin/users/
+├── index.blade.php # Daftar pengguna
+├── create.blade.php # Form tambah pengguna
+├── edit.blade.php # Form edit pengguna
+└── show.blade.php # Detail pengguna
+
+resources/views/layouts/
+└── app.blade.php # Layout utama
+
+database/seeders/
+└── UserManagementSeeder.php # Seeder untuk data awal
+```
+
+## 🎨 UI Components
+
+### Index Page
+
+- Tabel responsif dengan pagination
+- Badge untuk menampilkan role
+- Action buttons (View, Edit, Delete)
+- Modal konfirmasi untuk delete
+- Alert untuk feedback
+
+### Create/Edit Forms
+
+- Form validation dengan error display
+- Password strength requirements
+- Role selection dengan checkbox
+- Responsive layout (2 kolom)
+
+### Show Page
+
+- Informasi lengkap pengguna
+- Role dan permissions display
+- Activity summary cards
+- Action buttons
+
+## 🔧 Setup & Installation
+
+### 1. Run Migrations
+
+```bash
+php artisan migrate
+```
+
+### 2. Run Seeders
+
+```bash
+php artisan db:seed
+```
+
+### 3. Default Users
+
+Sistem sudah memiliki user default dari UserSeeder:
+
+- **ammar@dinaslhdki.id** (role: DLH) - Super admin dengan akses penuh
+- **kadis@dinaslhdki.id** (role: Kadis) - Management level access
+- **ppkl@dinaslhkdki.id** (role: PPKL) - Operational level access
+
+Password sesuai dengan yang sudah dikonfigurasi di UserSeeder.
+
+## 🚀 Usage Examples
+
+### Akses User Management
+
+1. Login sebagai user dengan role DLH (ammar@dinaslhdki.id)
+2. Klik menu "Pengaturan" → "Manajemen Pengguna"
+3. Atau akses langsung: `/admin/users`
+
+### Tambah User Baru
+
+1. Klik tombol "Tambah Pengguna"
+2. Isi form dengan data lengkap
+3. Pilih role yang sesuai
+4. Klik "Simpan"
+
+### Edit User
+
+1. Dari daftar user, klik tombol "Edit" (ikon pensil)
+2. Update informasi yang diperlukan
+3. Password bisa dikosongkan jika tidak ingin diubah
+4. Klik "Perbarui"
+
+### Hapus User
+
+1. Klik tombol "Hapus" (ikon trash)
+2. Konfirmasi di modal yang muncul
+3. User akan dihapus permanent
+
+## 🔒 Security Features
+
+### Password Requirements
+
+- Minimal 8 karakter
+- Harus mengandung huruf besar
+- Harus mengandung huruf kecil
+- Harus mengandung angka
+- Harus mengandung simbol khusus
+
+### Access Control
+
+- Hanya user dengan permission `settings.manage` yang bisa akses
+- User tidak bisa menghapus akun sendiri
+- CSRF protection pada semua form
+- Input validation dan sanitization
+
+### Role Management
+
+- Role assignment saat create/edit user
+- Multiple role support
+- Permission inheritance dari role
+- Real-time role sync
+
+## 🐛 Troubleshooting
+
+### Common Issues
+
+**Permission Denied**
+
+- Pastikan user memiliki permission `settings.manage`
+- Check role assignment di database
+
+**Validation Errors**
+
+- Email/username sudah digunakan
+- Password tidak memenuhi requirements
+- Role tidak valid
+
+**Layout Issues**
+
+- Pastikan Bootstrap CSS/JS ter-load
+- Check Lucide icons script
+- Verify Vite assets compiled
+
+### Debug Commands
+
+```bash
+# Check user permissions
+php artisan tinker
+>>> $user = User::find(1);
+>>> $user->getAllPermissions();
+
+# Reset permissions
+php artisan permission:cache-reset
+
+# Re-run seeders
+php artisan db:seed --class=UserManagementSeeder
+```
+
+## 📈 Future Enhancements
+
+- [ ] Bulk user operations
+- [ ] User import/export
+- [ ] Advanced filtering dan search
+- [ ] User activity logging
+- [ ] Email verification workflow
+- [ ] Password reset functionality
+- [ ] User profile pictures
+- [ ] Two-factor authentication
diff --git a/resources/views/pengguna/index_user.blade.php b/resources/views/admin/pengguna/index.blade.php
similarity index 100%
rename from resources/views/pengguna/index_user.blade.php
rename to resources/views/admin/pengguna/index.blade.php
diff --git a/resources/views/admin/users/create.blade.php b/resources/views/admin/users/create.blade.php
new file mode 100644
index 0000000..971e1ec
--- /dev/null
+++ b/resources/views/admin/users/create.blade.php
@@ -0,0 +1,96 @@
+@extends('layout.layout')
+
+@php
+ $title = 'Tambah Pengguna';
+ $subTitle = 'Manajemen Pengguna';
+@endphp
+
+@section('content')
+
+
+
+
+
+
Tambah Pengguna
+
+
+
+
+ @if ($errors->any())
+
+
+ @foreach ($errors->all() as $error)
+ - {{ $error }}
+ @endforeach
+
+
+ @endif
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php
new file mode 100644
index 0000000..afcc2c9
--- /dev/null
+++ b/resources/views/admin/users/edit.blade.php
@@ -0,0 +1,117 @@
+@extends('layout.layout')
+
+@php
+ $title = 'Edit Pengguna';
+ $subTitle = 'Manajemen Pengguna';
+@endphp
+
+@section('content')
+
+
+
+
+
+
Edit Pengguna: {{ $user->name }}
+
+
+
+
+ @if ($errors->any())
+
+
+ @foreach ($errors->all() as $error)
+ - {{ $error }}
+ @endforeach
+
+
+ @endif
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php
new file mode 100644
index 0000000..c6bad79
--- /dev/null
+++ b/resources/views/admin/users/index.blade.php
@@ -0,0 +1,149 @@
+@extends('layout.layout')
+
+@php
+ $title = 'Daftar Pengguna';
+ $subTitle = 'Manajemen Pengguna';
+ $script = '
+ ';
+@endphp
+
+@section('content')
+
+
+
+
+
Daftar Pengguna
+
+
+
+
+ @if (session('success'))
+
{{ session('success') }}
+ @endif
+
+ @if (session('error'))
+
{{ session('error') }}
+ @endif
+
+
+
+
+
+ No |
+ Nama |
+ Email |
+ Username |
+ Role |
+ Dibuat |
+ Aksi |
+
+
+
+ @foreach ($users as $user)
+
+
+ {{ isset($users->firstItem) ? $users->firstItem() + $loop->index : $loop->iteration }}
+ |
+ {{ $user->name }} |
+ {{ $user->email }} |
+ {{ $user->username }} |
+
+ @if ($user->roles->count() > 0)
+ @foreach ($user->roles as $role)
+ {{ $role->name }}
+ @endforeach
+ @else
+ Tidak ada role
+ @endif
+ |
+ {{ $user->created_at->format('d/m/Y H:i') }} |
+
+
+
+
+
+
+
+
+ @if ($user->id !== auth()->id())
+
+ @endif
+
+ |
+
+ @endforeach
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hapus Pengguna
+
Apakah Anda yakin ingin menghapus pengguna ? Tindakan ini tidak dapat dibatalkan.
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/admin/users/show.blade.php b/resources/views/admin/users/show.blade.php
new file mode 100644
index 0000000..0fff25a
--- /dev/null
+++ b/resources/views/admin/users/show.blade.php
@@ -0,0 +1,227 @@
+@extends('layout.layout')
+
+@php
+ $title = 'Detail Pengguna';
+ $subTitle = 'Manajemen Pengguna';
+ $script = '
+ ';
+@endphp
+
+@section('content')
+
+
+
+
+
+
Detail Pengguna: {{ $user->name }}
+
+
+
+
+
+
+
+
+
+
+
+ ID: |
+ {{ $user->id }} |
+
+
+ Nama: |
+ {{ $user->name }} |
+
+
+ Email: |
+
+ {{ $user->email }}
+ @if ($user->email_verified_at)
+ Terverifikasi
+ @else
+ Belum Terverifikasi
+ @endif
+ |
+
+
+ Username: |
+ {{ $user->username }} |
+
+
+ Dibuat: |
+ {{ $user->created_at->format('d/m/Y H:i:s') }} |
+
+
+ Diperbarui: |
+ {{ $user->updated_at->format('d/m/Y H:i:s') }} |
+
+
+
+
+
+
+
+
+
+
+
+ Role:
+ @if ($user->roles->count() > 0)
+ @foreach ($user->roles as $role)
+ {{ $role->name }}
+ @endforeach
+ @else
+ Tidak ada role
+ @endif
+
+
+
+
Permissions:
+ @if ($user->getAllPermissions()->count() > 0)
+
+ @foreach ($user->getAllPermissions()->chunk(ceil($user->getAllPermissions()->count() / 2)) as $permissionChunk)
+
+ @foreach ($permissionChunk as $permission)
+ {{ $permission->name }}
+ @endforeach
+
+ @endforeach
+
+ @else
+
Tidak ada permission
+ @endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Total Role
+ {{ $user->roles->count() }}
+
+
+
+
+
+
+
Total Permission
+ {{ $user->getAllPermissions()->count() }}
+
+
+
+
+
+
+
Status Email
+ @if ($user->email_verified_at)
+ ✓
+ @else
+ ✗
+ @endif
+
+
+
+
+
+
+
Akun Aktif
+ {{ $user->created_at->diffInDays(now()) }} hari
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit
+
+ @if ($user->id !== auth()->id())
+
+ @endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hapus Pengguna
+
Apakah Anda yakin ingin menghapus pengguna ? Tindakan ini tidak dapat dibatalkan.
+
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/auth/signin.blade.php b/resources/views/auth/signin.blade.php
index 72ab58d..bbf0b77 100644
--- a/resources/views/auth/signin.blade.php
+++ b/resources/views/auth/signin.blade.php
@@ -40,14 +40,7 @@
- {{-- --}}
+
@@ -57,6 +50,12 @@
+ @endguest
+
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
@vite(['resources/css/app.css', 'resources/js/app.js'])
@else
diff --git a/resources/views/components/head.blade.php b/resources/views/components/head.blade.php
index fd66f8e..b306706 100644
--- a/resources/views/components/head.blade.php
+++ b/resources/views/components/head.blade.php
@@ -61,6 +61,15 @@
{!! $css ?? '' !!}
@stack('css')
+ @guest
+
+ @endguest
+
diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php
index 24f4dd2..f2e05b9 100644
--- a/resources/views/components/navbar.blade.php
+++ b/resources/views/components/navbar.blade.php
@@ -236,6 +236,10 @@
+
diff --git a/resources/views/components/sidebar.blade.php b/resources/views/components/sidebar.blade.php
index b29a2b6..f0a094e 100644
--- a/resources/views/components/sidebar.blade.php
+++ b/resources/views/components/sidebar.blade.php
@@ -13,260 +13,260 @@
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
new file mode 100644
index 0000000..8454a84
--- /dev/null
+++ b/resources/views/layouts/app.blade.php
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+ @yield('title', 'Dashboard') - {{ config('app.name', 'Perling') }}
+
+
+
+
+
+
+
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
+
+ @stack('styles')
+
+
+
+
+
+
+
+
+
+ @yield('content')
+
+
+
+
+
+
+
+
+
+ @stack('scripts')
+
+
+
diff --git a/resources/views/pengguna/roles_user.blade.php b/resources/views/pengguna/roles_user.blade.php
deleted file mode 100644
index e69de29..0000000
diff --git a/routes/api.php b/routes/api.php
index 9769c6d..8dd9473 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -2,12 +2,27 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
+use App\Http\Controllers\DashboardController;
+// API Authentication Routes (Token-based)
Route::prefix('auth')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
+
Route::middleware('auth:sanctum')->group(function () {
+ Route::get('/me', [AuthController::class, 'me']);
Route::post('/logout', [AuthController::class, 'logout']);
Route::post('/logout-all', [AuthController::class, 'logoutAll']);
- Route::get('/me', [AuthController::class, 'me']);
+ });
+});
+
+// Protected API Routes
+Route::middleware('auth:sanctum')->group(function () {
+ // Dashboard API endpoints
+ Route::prefix('dashboard')->group(function () {
+ Route::get('/summary', [DashboardController::class, 'apiSummary']);
+ Route::get('/pertek', [DashboardController::class, 'apiPertek']);
+ Route::get('/rintek', [DashboardController::class, 'apiRintek']);
+ Route::get('/amdal', [DashboardController::class, 'apiAmdal']);
+ Route::get('/bengkel', [DashboardController::class, 'apiBengkel']);
});
});
diff --git a/routes/web.php b/routes/web.php
index dc8e866..e661273 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -6,10 +6,10 @@ use App\Http\Controllers\Izin\IzinAngkutController;
use App\Http\Controllers\Izin\IzinEmisiController;
use App\Http\Controllers\JadwalSidangController;
use App\Http\Controllers\LoginController;
-use App\Http\Controllers\AuthController as WebAuthController;
+use App\Http\Controllers\WebAuthController;
use App\Http\Controllers\NewsController;
use App\Http\Controllers\PenugasanController;
-use App\Http\Controllers\PerizinanLingkunganController;
+
use App\Http\Controllers\Admin\RoleController;
use App\Http\Controllers\Admin\PermissionController;
use App\Http\Controllers\Persetujuan\AddendumController;
@@ -50,7 +50,7 @@ Route::get('/surat/pertek/penerimaan', function () {
});
Route::get('/admin/pengguna', function () {
- return view('/pengguna/index_user');
+ return view('/admin/pengguna');
});
Route::get('/news', [NewsController::class, 'index'])->name('news.index');
@@ -61,22 +61,35 @@ Route::post('/surat/save', [SuratArahanController::class, 'save'])->name('surat.
// Route::get('/surat/exportPDF', [SuratArahanController::class, 'exportPDF'])->name('surat.exportPDF');
Route::match(['get', 'post'], '/surat/exportPDF', [SuratArahanController::class, 'exportPDF'])->name('surat.exportPDF');
-// Dashboard
-Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard.index');
-Route::get('/dashboard-pertek', [DashboardController::class, 'pertek'])->middleware('permission:dashboard.view.pertek')->name('dashboard.pertek');
-Route::get('/dashboard-rintek', [DashboardController::class, 'rintek'])->middleware('permission:dashboard.view.rintek')->name('dashboard.rintek');
-Route::get('/dashboard-amdal', [DashboardController::class, 'amdal'])->middleware('permission:dashboard.view.amdal')->name('dashboard.amdal');
-Route::get('/dashboard-bengkel', [DashboardController::class, 'bengkel'])->middleware('permission:dashboard.view.uji_emisi')->name('dashboard.bengkel');
+// Protected Web Routes (Session-based authentication)
+Route::middleware(['web.auth'])->group(function () {
+ // Dashboard
+ Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard.index');
+ Route::get('/dashboard-pertek', [DashboardController::class, 'pertek'])->middleware('permission:dashboard.view.pertek')->name('dashboard.pertek');
+ Route::get('/dashboard-rintek', [DashboardController::class, 'rintek'])->middleware('permission:dashboard.view.rintek')->name('dashboard.rintek');
+ Route::get('/dashboard-amdal', [DashboardController::class, 'amdal'])->middleware('permission:dashboard.view.amdal')->name('dashboard.amdal');
+ Route::get('/dashboard-bengkel', [DashboardController::class, 'bengkel'])->middleware('permission:dashboard.view.uji_emisi')->name('dashboard.bengkel');
+});
-// login
+// Web Authentication (Session-based with cookies)
Route::get('/auth/login', [LoginController::class, 'index'])->name('login.index');
Route::post('/auth/session-login', [WebAuthController::class, 'sessionLogin'])->name('login.session');
-Route::post('/auth/logout', [WebAuthController::class, 'logout'])->name('logout.session');
+Route::post('/auth/logout', [WebAuthController::class, 'sessionLogout'])->name('logout.session');
-// Roles & Permissions Management (restricted to settings.manage)
-Route::prefix('admin')->middleware('permission:settings.manage')->group(function () {
+// User Management & Settings (restricted to settings.manage)
+Route::prefix('admin')->middleware(['web.auth', 'permission:settings.manage'])->group(function () {
+ // User Management
+ Route::get('/users', [\App\Http\Controllers\Admin\UserController::class, 'index'])->name('admin.users.index');
+ Route::get('/users/create', [\App\Http\Controllers\Admin\UserController::class, 'create'])->name('admin.users.create');
+ Route::post('/users', [\App\Http\Controllers\Admin\UserController::class, 'store'])->name('admin.users.store');
+ Route::get('/users/{user}', [\App\Http\Controllers\Admin\UserController::class, 'show'])->name('admin.users.show');
+ Route::get('/users/{user}/edit', [\App\Http\Controllers\Admin\UserController::class, 'edit'])->name('admin.users.edit');
+ Route::put('/users/{user}', [\App\Http\Controllers\Admin\UserController::class, 'update'])->name('admin.users.update');
+ Route::delete('/users/{user}', [\App\Http\Controllers\Admin\UserController::class, 'destroy'])->name('admin.users.destroy');
+
+ // Roles & Permissions Management
Route::get('/roles', [RoleController::class, 'index'])->name('admin.roles.index');
Route::get('/roles/create', [RoleController::class, 'create'])->name('admin.roles.create');
Route::post('/roles', [RoleController::class, 'store'])->name('admin.roles.store');
@@ -93,7 +106,7 @@ Route::prefix('admin')->middleware('permission:settings.manage')->group(function
});
// Pertek
-Route::prefix('admin')->group(function () {
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/pertek/arahan', [PersetujuanTeknisController::class, 'index_arahan'])->middleware('permission:persetujuan_teknis.access')->name('pertek.index_arahan');
Route::get('/pertek/slo', [PersetujuanTeknisController::class, 'index_slo'])->middleware('permission:persetujuan_teknis.access')->name('pertek.index_slo');
Route::get('/pertek/detail-slo', [PersetujuanTeknisController::class, 'detail_slo'])->middleware('permission:persetujuan_teknis.access')->name('pertek.detail_slo');
@@ -107,19 +120,19 @@ Route::prefix('admin')->group(function () {
});
//rintek
-Route::prefix('admin')->group(function () {
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/rintek/arahan', [RincianTeknisController::class, 'index_arahan'])->middleware('permission:rincian_teknis.access')->name('rintek.index_arahan');
Route::get('/rintek/create-arahan', [RincianTeknisController::class, 'create_arahan'])->middleware('permission:rincian_teknis.access')->name('rintek.create_arahan');
Route::get('/rintek/verifikator/arahan', [RincianTeknisController::class, 'verifikator_arahan'])->middleware('permission:rincian_teknis.access')->name('rintek.verifikator_arahan');
});
//izin angkut
-Route::prefix('admin')->group(function () {
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/izinangkut', [IzinAngkutController::class, 'index_angkut'])->middleware('permission:izin_angkut_olah.access')->name('izinangkut.index_permohonan');
});
-//izin angkut
-Route::prefix('admin')->group(function () {
+//izin emisi
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/izinemisi', [IzinEmisiController::class, 'index_emisi'])->middleware('permission:izin_tempat_uji_emisi.access')->name('izinemisi.index_permohonan');
});
@@ -129,62 +142,59 @@ Route::prefix('admin')->group(function () {
// Route::get('/jadwal/create', [JadwalSidangController::class, 'create'])->name('jadwal.create');
// });
-Route::prefix('admin')->group(function () {
+// Jadwal Sidang
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/jadwal', [JadwalSidangController::class, 'index'])->middleware('permission:penjadwalan.access')->name('jadwal.index');
Route::match(['get', 'post'], '/jadwal/create', [JadwalSidangController::class, 'create'])->middleware('permission:penjadwalan.access')->name('jadwal.create');
});
// Penugasan
-Route::prefix('admin')->group(function () {
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/penugasan', [PenugasanController::class, 'index'])->name('penugasan.index');
});
-Route::prefix('admin')->group(function () {
+// Profile
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/profile', [ProfileController::class, 'index'])->name('profile.index');
});
-Route::prefix('admin')->group(function () {
+// News Management
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
Route::get('/news', [NewsController::class, 'index_newsvideo'])->name('news.index_newsvideo');
Route::get('/news/create', [NewsController::class, 'create_newsvideo'])->name('news.create_newsvideo');
});
-Route::prefix('admin')->group(function () {
+// Persetujuan Lingkungan Routes
+Route::prefix('admin')->middleware(['web.auth'])->group(function () {
+ // Kerangka Acuan
Route::get('/kerangka', [KerangkaController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.kerangka.index');
Route::get('/kerangka/create', [KerangkaController::class, 'create'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.kerangka.create');
-});
-Route::prefix('admin')->group(function () {
+ // AMDAL
Route::get('/amdal', [AmdalController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.amdal.index');
Route::get('/amdal/detail', [AmdalController::class, 'detail'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.amdal.detail');
-});
-Route::prefix('admin')->group(function () {
+ // UKL-UPL
Route::get('/ukl', [UklController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.ukl.index');
Route::get('/ukl/detail', [UklController::class, 'detail'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.ukl.detail');
-});
-Route::prefix('admin')->group(function () {
+ // RKL-RPL
Route::get('/rkl', [RklController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.rkl.index');
Route::get('/rkl/detail', [RklController::class, 'detail'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.rkl.detail');
-});
-Route::prefix('admin')->group(function () {
+ // SPPL
Route::get('/sppl', [SpplController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.sppl.index');
Route::get('/sppl/detail', [SpplController::class, 'detail'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.sppl.detail');
-});
-Route::prefix('admin')->group(function () {
+ // Addendum
Route::get('/addendum', [AddendumController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.addendum.index');
Route::get('/addendum/detail', [AddendumController::class, 'detail'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.addendum.detail');
-});
-Route::prefix('admin')->group(function () {
+ // DELH
Route::get('/delh', [DelhController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.delh.index');
Route::get('/delh/detail', [DelhController::class, 'detail'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.delh.detail');
-});
-
-Route::prefix('admin')->group(function () {
+ // DPLH
Route::get('/dplh', [DplhController::class, 'index'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.dplh.index');
Route::get('/dplh/detail', [DplhController::class, 'detail'])->middleware('permission:persetujuan_lingkungan.access')->name('persetujuan.dplh.detail');
});