update: roles permission

main
marszayn 2025-09-11 10:02:41 +07:00
parent 722115674f
commit 173ecf4834
27 changed files with 1782 additions and 344 deletions

2
.gitignore vendored
View File

@ -26,3 +26,5 @@ yarn-error.log
/.nova
/.vscode
/.zed
.kiro
.github

View File

@ -0,0 +1,150 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Spatie\Permission\Models\Role;
class UserController extends Controller
{
/**
* Display a listing of users
*/
public function index()
{
$users = User::with('roles')->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.');
}
}

View File

@ -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();
}
}
public function logoutAll(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['success' => true]);
}
// =============== Web (Session Guard) ===============
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();
}
if (!$user || !Hash::check($password, $user->password)) {
throw ValidationException::withMessages([
'identifier' => ['email/username/password yang diberikan salah.'],
return response()->json([
'success' => true,
'message' => 'Logout berhasil'
]);
}
Auth::login($user, true);
$request->session()->regenerate();
return response()->json(['success' => true]);
}
public function sessionLogout(Request $request)
/**
* Logout from all devices (revoke all tokens)
*/
public function logoutAll(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
$user = $request->user();
if ($request->expectsJson()) {
return response()->json(['success' => true]);
}
// Revoke all tokens for this user
$user->tokens()->delete();
return redirect()->route('login.index');
return response()->json([
'success' => true,
'message' => 'Logout dari semua perangkat berhasil'
]);
}
}

View File

@ -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,
],
]);
}
public function logout(Request $request)
return redirect()->intended('/dashboard');
}
/**
* 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');
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class WebAuthMiddleware
{
/**
* Handle an incoming request for web authentication
*/
public function handle(Request $request, Closure $next): Response
{
if (!Auth::guard('web')->check()) {
if ($request->expectsJson()) {
return response()->json([
'success' => false,
'message' => 'Unauthenticated'
], 401);
}
return redirect()->route('login.index');
}
return $next($request);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class ApiLoginRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'identifier' => ['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.',
];
}
}

View File

@ -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) {

View File

@ -40,6 +40,10 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'sanctum' => [
'driver' => 'sanctum',
'provider' => 'users',
],
],
/*

View File

@ -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,
]);
}

View File

@ -73,4 +73,3 @@ class RolesAndPermissionsSeeder extends Seeder
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
}
}

View File

@ -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',

View File

@ -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"
```

View File

@ -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

View File

@ -0,0 +1,96 @@
@extends('layout.layout')
@php
$title = 'Tambah Pengguna';
$subTitle = 'Manajemen Pengguna';
@endphp
@section('content')
<div class="card basic-data-table">
<div class="card-body">
<div
class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-3 gap-3">
<div>
<h5 class="mb-0">Tambah Pengguna</h5>
</div>
<div class="d-flex gap-2">
<a href="{{ route('admin.users.index') }}"
class="btn btn-secondary btn-sm d-flex align-items-center gap-2">
<iconify-icon icon="iconoir:arrow-left"></iconify-icon>
<span>Kembali</span>
</a>
</div>
</div>
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.users.store') }}">
@csrf
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label">Nama Lengkap</label>
<input type="text" name="name" class="form-control" required value="{{ old('name') }}"
placeholder="Masukkan nama lengkap">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" required value="{{ old('email') }}"
placeholder="Masukkan email">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control" required value="{{ old('username') }}"
placeholder="Masukkan username">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" required
placeholder="Masukkan password">
<div class="form-text">Password harus minimal 8 karakter, mengandung huruf besar, huruf kecil,
angka, dan simbol khusus.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Konfirmasi Password</label>
<input type="password" name="password_confirmation" class="form-control" required
placeholder="Konfirmasi password">
</div>
</div>
<div class="mt-4">
<label class="form-label">Roles</label>
<div class="row g-2">
@foreach ($roles as $role)
<div class="col-12 col-md-4">
<div class="form-check style-check d-flex align-items-center">
<input class="form-check-input border border-neutral-300 me-8" type="checkbox"
name="roles[]" value="{{ $role->name }}" id="role_{{ $role->id }}"
{{ in_array($role->name, old('roles', [])) ? 'checked' : '' }}>
<label class="form-check-label"
for="role_{{ $role->id }}">{{ $role->name }}</label>
</div>
</div>
@endforeach
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button class="btn btn-primary d-flex align-items-center gap-2">
<iconify-icon icon="material-symbols:save"></iconify-icon>
<span>Simpan</span>
</button>
<a href="{{ route('admin.users.index') }}" class="btn btn-light">Batal</a>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,117 @@
@extends('layout.layout')
@php
$title = 'Edit Pengguna';
$subTitle = 'Manajemen Pengguna';
@endphp
@section('content')
<div class="card basic-data-table">
<div class="card-body">
<div
class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-3 gap-3">
<div>
<h5 class="mb-0">Edit Pengguna: {{ $user->name }}</h5>
</div>
<div class="d-flex gap-2">
<a href="{{ route('admin.users.index') }}"
class="btn btn-secondary btn-sm d-flex align-items-center gap-2">
<iconify-icon icon="iconoir:arrow-left"></iconify-icon>
<span>Kembali</span>
</a>
</div>
</div>
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.users.update', $user) }}">
@csrf
@method('PUT')
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label">Nama Lengkap</label>
<input type="text" name="name" class="form-control" required
value="{{ old('name', $user->name) }}" placeholder="Masukkan nama lengkap">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" required
value="{{ old('email', $user->email) }}" placeholder="Masukkan email">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control" required
value="{{ old('username', $user->username) }}" placeholder="Masukkan username">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Password Baru</label>
<input type="password" name="password" class="form-control"
placeholder="Kosongkan jika tidak ingin mengubah">
<div class="form-text">Kosongkan jika tidak ingin mengubah password. Password harus minimal 8
karakter, mengandung huruf besar, huruf kecil, angka, dan simbol khusus.</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Konfirmasi Password Baru</label>
<input type="password" name="password_confirmation" class="form-control"
placeholder="Konfirmasi password baru">
</div>
</div>
<div class="mt-4">
<label class="form-label">Roles</label>
<div class="row g-2">
@foreach ($roles as $role)
<div class="col-12 col-md-4">
<div class="form-check style-check d-flex align-items-center">
<input class="form-check-input border border-neutral-300 me-8" type="checkbox"
name="roles[]" value="{{ $role->name }}" id="role_{{ $role->id }}"
{{ in_array($role->name, old('roles', $userRoles)) ? 'checked' : '' }}>
<label class="form-check-label"
for="role_{{ $role->id }}">{{ $role->name }}</label>
</div>
</div>
@endforeach
</div>
</div>
<!-- User Info -->
<div class="mt-4">
<div class="alert alert-info">
<h6>Informasi Pengguna:</h6>
<ul class="mb-0">
<li>Dibuat: {{ $user->created_at->format('d/m/Y H:i:s') }}</li>
<li>Terakhir diperbarui: {{ $user->updated_at->format('d/m/Y H:i:s') }}</li>
@if ($user->email_verified_at)
<li>Email diverifikasi: {{ $user->email_verified_at->format('d/m/Y H:i:s') }}</li>
@else
<li class="text-warning">Email belum diverifikasi</li>
@endif
</ul>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button class="btn btn-primary d-flex align-items-center gap-2">
<iconify-icon icon="material-symbols:save"></iconify-icon>
<span>Perbarui</span>
</button>
<a href="{{ route('admin.users.show', $user) }}" class="btn btn-info d-flex align-items-center gap-2">
<iconify-icon icon="lucide:eye"></iconify-icon>
<span>Lihat Detail</span>
</a>
<a href="{{ route('admin.users.index') }}" class="btn btn-light">Batal</a>
</div>
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,149 @@
@extends('layout.layout')
@php
$title = 'Daftar Pengguna';
$subTitle = 'Manajemen Pengguna';
$script = '
<script>
let table = new DataTable("#dataTable");
// Handle delete modal
document.addEventListener("DOMContentLoaded", function() {
const deleteModal = document.getElementById("deleteModal");
const deleteForm = document.getElementById("deleteForm");
const userNameText = document.getElementById("userNameText");
deleteModal.addEventListener("show.bs.modal", function(event) {
const button = event.relatedTarget;
const userName = button.getAttribute("data-user-name");
const userId = button.getAttribute("data-user-id");
userNameText.textContent = userName;
deleteForm.action = "' . route('admin.users.index') . '/" + userId;
});
});
</script>';
@endphp
@section('content')
<div class="card basic-data-table">
<div class="card-body">
<div
class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-3 gap-3">
<div>
<h5 class="mb-0">Daftar Pengguna</h5>
</div>
<div
class="d-flex flex-column flex-sm-row align-items-stretch align-items-sm-center justify-content-end gap-2 w-md-auto">
<div class="flex-fill flex-sm-fill-0">
<a href="{{ route('admin.users.create') }}"
class="btn btn-primary btn-sm d-flex align-items-center justify-content-center gap-2 w-100">
<iconify-icon icon="material-symbols:add" class="text-lg"></iconify-icon>
<span>Tambah Pengguna</span>
</a>
</div>
</div>
</div>
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if (session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="table-responsive">
<table class="table bordered-table mb-0" id="dataTable" data-page-length='10'>
<thead>
<tr>
<th scope="col" class="text-center">No</th>
<th scope="col" class="text-center">Nama</th>
<th scope="col" class="text-center">Email</th>
<th scope="col" class="text-center">Username</th>
<th scope="col" class="text-center">Role</th>
<th scope="col" class="text-center">Dibuat</th>
<th scope="col" class="text-center">Aksi</th>
</tr>
</thead>
<tbody>
@foreach ($users as $user)
<tr>
<td class="text-center">
{{ isset($users->firstItem) ? $users->firstItem() + $loop->index : $loop->iteration }}
</td>
<td class="text-center">{{ $user->name }}</td>
<td class="text-center">{{ $user->email }}</td>
<td class="text-center">{{ $user->username }}</td>
<td class="text-center">
@if ($user->roles->count() > 0)
@foreach ($user->roles as $role)
<span class="badge bg-primary me-1">{{ $role->name }}</span>
@endforeach
@else
<span class="badge bg-secondary">Tidak ada role</span>
@endif
</td>
<td class="text-center">{{ $user->created_at->format('d/m/Y H:i') }}</td>
<td class="text-center">
<div class="d-flex align-items-center gap-10 justify-content-center">
<a href="{{ route('admin.users.show', $user) }}"
class="bg-info-focus text-info-600 bg-hover-info-200 fw-medium w-40-px h-40-px d-flex justify-content-center align-items-center rounded-circle text-decoration-none"
title="Lihat Detail">
<iconify-icon icon="lucide:eye" class="menu-icon"></iconify-icon>
</a>
<a href="{{ route('admin.users.edit', $user) }}"
class="bg-success-focus text-success-600 bg-hover-success-200 fw-medium w-40-px h-40-px d-flex justify-content-center align-items-center rounded-circle text-decoration-none"
title="Edit Pengguna">
<iconify-icon icon="lucide:edit" class="menu-icon"></iconify-icon>
</a>
@if ($user->id !== auth()->id())
<button type="button"
class="remove-item-btn bg-danger-focus bg-hover-danger-200 text-danger-600 fw-medium w-40-px h-40-px d-flex justify-content-center align-items-center rounded-circle"
title="Hapus Pengguna" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-user-name="{{ $user->name }}" data-user-id="{{ $user->id }}">
<iconify-icon icon="fluent:delete-24-regular"
class="menu-icon"></iconify-icon>
</button>
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center px-4 pb-4">
<div class="mb-3">
<div
class="bg-danger-focus w-80-px h-80-px d-flex justify-content-center align-items-center rounded-circle mx-auto mb-3">
<iconify-icon icon="fluent:delete-24-regular" class="text-danger-600"
style="font-size: 2.5rem;"></iconify-icon>
</div>
<h4 class="mb-2 text-dark">Hapus Pengguna</h4>
<p class="text-secondary mb-0">Apakah Anda yakin ingin menghapus pengguna <strong
id="userNameText"></strong></br>? Tindakan ini tidak dapat dibatalkan.</p>
</div>
<div class="d-flex gap-2 justify-content-center pb-8">
<button type="button" class="btn btn-light-secondary px-4" data-bs-dismiss="modal">Batal</button>
<form id="deleteForm" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger px-4">Ya, Hapus</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,227 @@
@extends('layout.layout')
@php
$title = 'Detail Pengguna';
$subTitle = 'Manajemen Pengguna';
$script = '
<script>
// Handle delete modal
document.addEventListener("DOMContentLoaded", function() {
const deleteModal = document.getElementById("deleteModal");
const deleteForm = document.getElementById("deleteForm");
const userNameText = document.getElementById("userNameText");
if (deleteModal) {
deleteModal.addEventListener("show.bs.modal", function(event) {
const button = event.relatedTarget;
const userName = button.getAttribute("data-user-name");
const userId = button.getAttribute("data-user-id");
userNameText.textContent = userName;
deleteForm.action = "' . route('admin.users.index') . '/" + userId;
});
}
});
</script>';
@endphp
@section('content')
<div class="card basic-data-table">
<div class="card-body">
<div
class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-3 gap-3">
<div>
<h5 class="mb-0">Detail Pengguna: {{ $user->name }}</h5>
</div>
<div class="d-flex gap-2">
<a href="{{ route('admin.users.index') }}"
class="btn btn-secondary btn-sm d-flex align-items-center gap-2">
<iconify-icon icon="iconoir:arrow-left"></iconify-icon>
<span>Kembali</span>
</a>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<h6 class="mb-0">Informasi Dasar</h6>
</div>
<div class="card-body">
<table class="table table-borderless mb-0">
<tr>
<td width="150"><strong>ID:</strong></td>
<td>{{ $user->id }}</td>
</tr>
<tr>
<td><strong>Nama:</strong></td>
<td>{{ $user->name }}</td>
</tr>
<tr>
<td><strong>Email:</strong></td>
<td>
{{ $user->email }}
@if ($user->email_verified_at)
<span class="badge bg-success ms-2">Terverifikasi</span>
@else
<span class="badge bg-warning ms-2">Belum Terverifikasi</span>
@endif
</td>
</tr>
<tr>
<td><strong>Username:</strong></td>
<td>{{ $user->username }}</td>
</tr>
<tr>
<td><strong>Dibuat:</strong></td>
<td>{{ $user->created_at->format('d/m/Y H:i:s') }}</td>
</tr>
<tr>
<td><strong>Diperbarui:</strong></td>
<td>{{ $user->updated_at->format('d/m/Y H:i:s') }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">
<h6 class="mb-0">Role & Permissions</h6>
</div>
<div class="card-body">
<div class="mb-3">
<strong>Role:</strong><br>
@if ($user->roles->count() > 0)
@foreach ($user->roles as $role)
<span class="badge bg-primary me-1 mb-1">{{ $role->name }}</span>
@endforeach
@else
<span class="badge bg-secondary">Tidak ada role</span>
@endif
</div>
<div class="mb-3">
<strong>Permissions:</strong><br>
@if ($user->getAllPermissions()->count() > 0)
<div class="row">
@foreach ($user->getAllPermissions()->chunk(ceil($user->getAllPermissions()->count() / 2)) as $permissionChunk)
<div class="col-12">
@foreach ($permissionChunk as $permission)
<span class="badge bg-info me-1 mb-1">{{ $permission->name }}</span>
@endforeach
</div>
@endforeach
</div>
@else
<span class="badge bg-secondary">Tidak ada permission</span>
@endif
</div>
</div>
</div>
</div>
</div>
<!-- Activity Summary -->
<div class="row g-4 mt-2">
<div class="col-12">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Ringkasan Aktivitas</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<div class="card bg-light text-center">
<div class="card-body">
<h6 class="card-title">Total Role</h6>
<h3 class="text-primary mb-0">{{ $user->roles->count() }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-light text-center">
<div class="card-body">
<h6 class="card-title">Total Permission</h6>
<h3 class="text-info mb-0">{{ $user->getAllPermissions()->count() }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-light text-center">
<div class="card-body">
<h6 class="card-title">Status Email</h6>
@if ($user->email_verified_at)
<h3 class="text-success mb-0"></h3>
@else
<h3 class="text-warning mb-0"></h3>
@endif
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-light text-center">
<div class="card-body">
<h6 class="card-title">Akun Aktif</h6>
<h3 class="text-success mb-0">{{ $user->created_at->diffInDays(now()) }} hari
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<a href="{{ route('admin.users.edit', $user) }}" class="btn btn-primary d-flex align-items-center gap-2">
<iconify-icon icon="lucide:edit"></iconify-icon>
<span>Edit</span>
</a>
@if ($user->id !== auth()->id())
<button type="button" class="btn btn-danger d-flex align-items-center gap-2" data-bs-toggle="modal"
data-bs-target="#deleteModal" data-user-name="{{ $user->name }}"
data-user-id="{{ $user->id }}">
<iconify-icon icon="fluent:delete-24-regular"></iconify-icon>
<span>Hapus</span>
</button>
@endif
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center px-4 pb-4">
<div class="mb-3">
<div
class="bg-danger-focus w-80-px h-80-px d-flex justify-content-center align-items-center rounded-circle mx-auto mb-3">
<iconify-icon icon="fluent:delete-24-regular" class="text-danger-600 text-4xl"></iconify-icon>
</div>
<h4 class="mb-2 text-dark">Hapus Pengguna</h4>
<p class="text-secondary mb-0">Apakah Anda yakin ingin menghapus pengguna <strong
id="userNameText"></strong>? Tindakan ini tidak dapat dibatalkan.</p>
</div>
<div class="d-flex gap-2 justify-content-center">
<button type="button" class="btn btn-light-secondary px-4" data-bs-dismiss="modal">Batal</button>
<form id="deleteForm" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger px-4">Ya, Hapus</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -40,14 +40,7 @@
<small id="login-error" class="text-danger" style="display:none;"></small>
</div>
{{-- <div class="">
<div class="d-flex justify-content-between gap-2">
<div class="form-check style-check d-flex align-items-center">
<input class="form-check-input border border-neutral-300" type="checkbox" value="" id="remember">
<label class="form-check-label" for="remember">Ingat saya</label>
</div>
</div>
</div> --}}
<button id="btn-login" type="submit" class="login-button">Login</button>
</form>
</div>
@ -57,6 +50,12 @@
<x-script />
<script>
// Clear any leftover tokens on signin page
try {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
} catch (e) {}
document.querySelector('.password-toggle').addEventListener('click', function() {
const passwordInput = document.getElementById('password');
const icon = this.querySelector('i');

View File

@ -3,6 +3,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Sistem Perizinan Lingkungan')</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" type="image/ico" href="{{ asset('favicon.ico') }}" sizes="16x16">
@ -32,6 +33,15 @@
@stack('css')
@guest
<script>
try {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
} catch (e) {}
</script>
@endguest
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
@vite(['resources/css/app.css', 'resources/js/app.js'])
@else

View File

@ -61,6 +61,15 @@
{!! $css ?? '' !!}
@stack('css')
@guest
<script>
try {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
} catch (e) {}
</script>
@endguest
<!-- Include select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/css/select2.min.css" rel="stylesheet" />
</head>

View File

@ -236,6 +236,10 @@
</div>
</div>
</div>
<form id="logout-form" action="{{ route('logout.session') }}" method="POST" class="d-none">
@csrf
<input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>
<script>
(function(){
const btn = document.getElementById('btn-logout');
@ -252,19 +256,15 @@
}
} catch (err) { /* ignore */ }
try {
await fetch('{{ route('logout.session') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json'
}
}).catch(()=>{});
} catch (err) { /* ignore */ }
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_user');
// Submit standard POST form to ensure session logout with CSRF
const form = document.getElementById('logout-form');
if (form) {
form.submit();
} else {
window.location.href = '{{ route('login.index') }}';
}
});
})();
</script>

View File

@ -113,7 +113,7 @@
</a>
</li>
<li>
<a href="{{ route ('persetujuan.amdal.index') }}"><i class="text-primary-600 w-auto"></i>
<a href="{{ route('persetujuan.amdal.index') }}"><i class="text-primary-600 w-auto"></i>
<iconify-icon icon="bi:journal-text" class="menu-icon"></iconify-icon>
<span>Amdal</span>
</a>
@ -199,7 +199,7 @@
{{-- submenu --}}
<ul class="sidebar-submenu">
<li>
<a href="/admin/pengguna"><i class="text-primary-600 w-auto"></i>
<a href="{{ route('admin.users.index') }}"><i class="text-primary-600 w-auto"></i>
<iconify-icon icon="bi:person-fill" class="menu-icon"></iconify-icon>
<span>Pengguna</span>
</a>

View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'Dashboard') - {{ config('app.name', 'Perling') }}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('styles')
</head>
<body>
<div id="app">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="{{ route('dashboard.index') }}">
{{ config('app.name', 'Perling') }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ route('dashboard.index') }}">Dashboard</a>
</li>
@can('settings.manage')
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
Pengaturan
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ route('admin.users.index') }}">Manajemen
Pengguna</a></li>
<li><a class="dropdown-item" href="{{ route('admin.roles.index') }}">Manajemen Role</a>
</li>
<li><a class="dropdown-item" href="{{ route('admin.permissions.index') }}">Manajemen
Permission</a></li>
</ul>
</li>
@endcan
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
{{ Auth::user()->name }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ route('profile.index') }}">Profile</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<form method="POST" action="{{ route('logout.session') }}">
@csrf
<button type="submit" class="dropdown-item">Logout</button>
</form>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="py-4">
@yield('content')
</main>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Initialize Lucide Icons -->
<script>
lucide.createIcons();
</script>
@stack('scripts')
</body>
</html>

View File

@ -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']);
});
});

View File

@ -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');
});