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 + +
+ @csrf +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Password harus minimal 8 karakter, mengandung huruf besar, huruf kecil, + angka, dan simbol khusus.
+
+
+ + +
+
+ +
+ +
+ @foreach ($roles as $role) +
+
+ name, old('roles', [])) ? 'checked' : '' }}> + +
+
+ @endforeach +
+
+ +
+ + Batal +
+
+
+
+ +@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 + +
+ @csrf + @method('PUT') +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Kosongkan jika tidak ingin mengubah password. Password harus minimal 8 + karakter, mengandung huruf besar, huruf kecil, angka, dan simbol khusus.
+
+
+ + +
+
+ +
+ +
+ @foreach ($roles as $role) +
+
+ name, old('roles', $userRoles)) ? 'checked' : '' }}> + +
+
+ @endforeach +
+
+ + +
+
+
Informasi Pengguna:
+
    +
  • Dibuat: {{ $user->created_at->format('d/m/Y H:i:s') }}
  • +
  • Terakhir diperbarui: {{ $user->updated_at->format('d/m/Y H:i:s') }}
  • + @if ($user->email_verified_at) +
  • Email diverifikasi: {{ $user->email_verified_at->format('d/m/Y H:i:s') }}
  • + @else +
  • Email belum diverifikasi
  • + @endif +
+
+
+ +
+ + + + Lihat Detail + + Batal +
+
+
+
+ +@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 + +
+ + + + + + + + + + + + + + @foreach ($users as $user) + + + + + + + + + + @endforeach + +
NoNamaEmailUsernameRoleDibuatAksi
+ {{ 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 +
+
+
+
+
+ + + +@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 }}
+
+ +
+ +
+
+
+
+
Informasi Dasar
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
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 & Permissions
+
+
+
+ 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 +
+
+
+
+
+ + +
+
+
+
+
Ringkasan Aktivitas
+
+
+
+
+
+
+
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 +
+
+
+ + + + +@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 @@ +
+ @csrf + +
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'); });