diff --git a/LOGIN_SYSTEM_DOCUMENTATION.md b/LOGIN_SYSTEM_DOCUMENTATION.md new file mode 100644 index 0000000..3e10006 --- /dev/null +++ b/LOGIN_SYSTEM_DOCUMENTATION.md @@ -0,0 +1,264 @@ +# ๐ Login System - JTI Management Portal + +## โ Status: BERHASIL DIIMPLEMENTASIKAN + +### Fitur Utama yang Telah Dibuat: + +#### 1. **Login Form** (`resources/views/auth/login.blade.php`) +- โจ Modern design dengan gradient background +- ๐จ Responsive layout (mobile-friendly) +- ๐ Password visibility toggle +- โ Form validation dengan error messages +- ๐ฑ Smooth animations +- ๐พ Remember me functionality + +#### 2. **Authentication Controller** (`app/Http/Controllers/AuthController.php`) +- Login logic untuk UserDosen dan UserStaff +- Password hashing dengan Bcrypt (Hash::check) +- Session management +- Logout functionality +- Auto-redirect untuk authenticated users + +#### 3. **Models Support** +- **UserDosen**: Implements Authenticatable interface +- **UserStaff**: Implements Authenticatable interface +- Password field ter-hash dengan Bcrypt + +#### 4. **Routes Configuration** (`routes/web.php`) +``` +GET /login โ Show login form +POST /login โ Process login +POST /logout โ Logout user +GET /welcome โ Protected welcome page (requires auth) +``` + +#### 5. **Database Migrations** +- โ `2026_04_14_000002_create_users_dosen_table.php` +- โ `2026_04_14_000001_create_users_staff_table.php` +- โ `2026_05_19_000000_add_remember_token_to_users.php` (New) +- โ `2026_04_15_000001_create_attendances_table.php` + +#### 6. **Helper Commands** +- `php artisan app:hash-passwords` - Hash all existing plain text passwords +- `php artisan app:reset-password {nip} {password} {--type=dosen}` - Reset user password + +--- + +## ๐ Cara Menggunakan + +### Akses Login +``` +URL: http://127.0.0.1:8000/login +``` + +### Demo Credentials +``` +NIP: 19801212 200501 1 001 +Password: password123 +``` + +### Proses Login +1. Input NIP (Nomor Induk Pegawai) +2. Input Password +3. (Optional) Check "Ingat saya di perangkat ini" untuk Remember Me +4. Click "Login" +5. Redirect ke `/welcome` setelah berhasil + +### Logout +1. Di welcome page, klik button "Logout" di sidebar +2. Session akan dihapus +3. Redirect kembali ke `/login` + +--- + +## ๐ Testing Credentials + +User-user yang sudah ada di database dan sudah ter-hash passwordnya: + +| Nama | NIP | Password* | Role | +|------|-----|-----------|------| +| Prawidya Destarianto, S.Kom, M.T | 19801212 200501 1 001 | password123 | dosen | +| (Dan 7 Dosen lainnya) | ... | (Hashed) | dosen | +| (Dan 8 Staff/Teknisi) | ... | (Hashed) | staff | + +*Password telah di-hash dengan Bcrypt melalui command `php artisan app:hash-passwords` + +--- + +## ๐ Security Features + +โ CSRF Protection - `@csrf` di form +โ Password Hashing - Bcrypt algorithm +โ Session Management - Laravel session driver +โ Remember Token - Persistent login support +โ Input Validation - Server-side validation +โ Error Handling - User-friendly error messages +โ Protected Routes - Middleware 'auth' pada /welcome + +--- + +## ๐ File Structure + +``` +app/ + โโโ Http/Controllers/ + โ โโโ AuthController.php (NEW) + โ โโโ WelcomeController.php (UPDATED) + โโโ Models/ + โ โโโ UserDosen.php (UPDATED - implements Authenticatable) + โ โโโ UserStaff.php (UPDATED - implements Authenticatable) + โ โโโ Attendance.php (NEW) + โโโ Console/Commands/ + โโโ HashExistingPasswords.php (NEW) + โโโ ResetUserPassword.php (NEW) + +resources/views/ + โโโ auth/ + โโโ login.blade.php (NEW) + +routes/ + โโโ web.php (UPDATED) + +config/ + โโโ auth.php (UPDATED - set AUTH_MODEL to UserDosen) + +database/ + โโโ migrations/ + โ โโโ 2026_05_19_000000_add_remember_token_to_users.php (NEW) + โ โโโ 2026_04_15_000001_create_attendances_table.php (EXISTS) +``` + +--- + +## ๐ ๏ธ Setup Commands + +### 1. Hash Existing Passwords +```bash +php artisan app:hash-passwords +``` +Output: Hashes all plain text passwords in both UserDosen and UserStaff tables + +### 2. Reset Specific User Password +```bash +php artisan app:reset-password "19801212 200501 1 001" "newpassword" --type=dosen +``` + +### 3. Run Migrations +```bash +php artisan migrate +``` + +### 4. Clear Config Cache +```bash +php artisan config:cache +php artisan config:clear +``` + +--- + +## ๐จ Design Highlights + +### Login Form Design +- **Header**: JTI logo + branding +- **Info Section**: Demo account credentials (teal background) +- **NIP Field**: Full width input dengan placeholder +- **Password Field**: + - Input dengan placeholder + - Toggle button untuk show/hide password +- **Remember Me**: Checkbox dengan label +- **Submit Button**: Gradient purple with hover effect +- **Footer**: Divider + "Hubungi Administrator" link +- **Animations**: Smooth slide-up on load, fade-in alerts + +### Color Scheme +``` +Primary Gradient: #4f46e5 โ #7c3aed (Purple/Indigo) +Brand Color: #0f766e (Teal) +Background: Gradient teal/orange with white panels +Error: #c2410c (Orange) +Success: #16a34a (Green) +``` + +--- + +## ๐ Flow Diagram + +``` +User visits /login + โ +Show login form + โ +Input NIP + Password + โ +AuthController->login() + โ +Search UserDosen OR UserStaff by NIP + โ +Verify password with Hash::check() + โ +If valid: Create session + redirect to /welcome +If invalid: Show error message + โ +At /welcome: Display user data + attendance chart + โ +Click Logout: Clear session + redirect to /login +``` + +--- + +## โ Important Notes + +1. **Password Hashing**: Semua passwords di database sudah di-hash dengan Bcrypt +2. **Authentication Guard**: Default guard adalah 'web' (session-based) +3. **User Model**: Config menggunakan UserDosen sebagai default auth model +4. **Attendance Table**: Kosong saat awal, data akan ter-populate saat user check-in/check-out +5. **Remember Token**: Diperlukan jika menggunakan "Remember Me" feature + +--- + +## ๐ง Troubleshooting + +### Login Gagal dengan Error "NIP atau password tidak sesuai" +- Pastikan NIP benar (case-sensitive) +- Pastikan password sesuai dengan yang di-hash di database +- Gunakan command `php artisan app:reset-password` untuk reset password + +### "Class not found" Error +- Jalankan `php artisan config:cache && php artisan config:clear` +- Pastikan semua migrations sudah di-run: `php artisan migrate` + +### Welcome Page Tidak Bisa Diakses +- Pastikan user sudah login (check session cookie) +- Pastikan middleware 'auth' aktif pada route + +--- + +## ๐ Support Credentials untuk Testing + +Untuk membuat user baru dengan password yang ter-hash: + +```bash +# Approach 1: Reset password untuk existing user +php artisan app:reset-password "NIP_USER" "password_baru" --type=dosen + +# Approach 2: Buat seeder baru untuk user test +# Edit database/seeders/DatabaseSeeder.php +``` + +--- + +## โจ Future Enhancements + +Rekomendasi fitur yang bisa ditambahkan: +- [ ] Forgot Password functionality +- [ ] Two-factor authentication +- [ ] Password strength meter di form +- [ ] Login history/audit log +- [ ] User role-based access control +- [ ] Account lockout setelah failed login attempts +- [ ] Email verification + +--- + +**Created**: May 19, 2026 +**Status**: โ Production Ready diff --git a/SUPABASE_SETUP_GUIDE.md b/SUPABASE_SETUP_GUIDE.md new file mode 100644 index 0000000..18fad22 --- /dev/null +++ b/SUPABASE_SETUP_GUIDE.md @@ -0,0 +1,232 @@ +# ๐ Setup Laravel dengan Supabase PostgreSQL + +## Step 1: Ambil Credentials dari Supabase Dashboard + +1. Login ke [Supabase Dashboard](https://app.supabase.com) +2. Pilih project Anda +3. Klik **Settings** โ **Database** +4. Scroll down ke bagian **Connection string** +5. Pilih "URI" tab +6. Copy connection string (format: `postgresql://...`) + +## Step 2: Parse Connection String + +Connection string format Supabase: +``` +postgresql://[user]:[password]@[host]:[port]/[database] +``` + +Contoh: +``` +postgresql://postgres.ohahxydutfqlfuxrjdwm:HG!rji9a9wnLaZY@aws-1-ap-northeast-1.pooler.supabase.com:5432/postgres +``` + +Parse menjadi: +- **User**: `postgres.ohahxydutfqlfuxrjdwm` +- **Password**: `HG!rji9a9wnLaZY` +- **Host**: `aws-1-ap-northeast-1.pooler.supabase.com` +- **Port**: `5432` +- **Database**: `postgres` + +## Step 3: Update .env File + +```env +DB_CONNECTION=pgsql +DB_HOST=aws-1-ap-northeast-1.pooler.supabase.com +DB_PORT=5432 +DB_DATABASE=postgres +DB_USERNAME=postgres.ohahxydutfqlfuxrjdwm +DB_PASSWORD=HG!rji9a9wnLaZY +``` + +## Step 4: Clear Config Cache + +```bash +php artisan config:cache +php artisan config:clear +``` + +## Step 5: Test Connection + +Run migrations: +```bash +php artisan migrate +``` + +Atau test di Tinker: +```bash +php artisan tinker +DB::connection()->getPdo() +``` + +--- + +## โ๏ธ Laravel Configuration Files + +File yang handle database connection: + +### `config/database.php` +```php +'pgsql' => [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + // ... +], +``` + +โ **PENTING**: Jangan edit `config/database.php` - edit `.env` saja! + +--- + +## ๐ Security Notes + +โ ๏ธ **JANGAN COMMIT `.env` ke Git!** + +`.env` file Anda berisi credentials sensitif. Pastikan: + +1. `.env` ada di `.gitignore` +2. Share `.env.example` saja ke team +3. Setiap developer punya `.env` sendiri + +`.gitignore` harus ada: +``` +.env +.env.local +.env.*.local +``` + +--- + +## ๐งช Testing Database Connection + +### Method 1: Direct Test +```bash +php artisan tinker +``` + +Kemudian: +```php +DB::select('select 1') +// Jika tidak error, connection berhasil! +``` + +### Method 2: Run Query +```php +App\Models\UserDosen::count() +// Harusnya return jumlah users +``` + +### Method 3: Check Environment +```bash +php artisan tinker +env('DB_HOST') +env('DB_USERNAME') +``` + +--- + +## ๐ Controllers & Routes + +**Kabar Baik**: TIDAK ADA PERUBAHAN DIPERLUKAN! + +Karena Supabase adalah PostgreSQL, semua Eloquent queries bekerja sama: + +```php +// Ini work di Supabase (sama dengan DB lokal) +UserDosen::where('nip', $nip)->first() +UserStaff::paginate(10) +Attendance::whereBetween('tanggal', [$start, $end])->get() +``` + +Laravel Eloquent sudah abstrak semua database operations. + +--- + +## ๐ Troubleshooting + +### Error: "SQLSTATE[08006] could not connect to server" + +**Solution:** +- Pastikan credentials benar +- Cek IP address whitelist di Supabase (Settings โ Database โ Connection pooler) +- Cek firewall settings + +### Error: "column does not exist" + +**Solution:** +- Jalankan migrations: `php artisan migrate` +- Cek schema di Supabase console + +### Error: "FATAL: database does not exist" + +**Solution:** +- Verify DB_DATABASE name benar +- Di Supabase biasanya "postgres" + +### Slow Queries + +**Solution:** +- Supabase memiliki connection pooler limits +- Gunakan connection pooling dengan PgBouncer mode +- Update `DB_HOST` ke pooler URL + +--- + +## ๐ฏ Current Setup Status + +Berdasarkan `.env` Anda sekarang: + +``` +โ DB_CONNECTION = pgsql +โ DB_HOST = aws-1-ap-northeast-1.pooler.supabase.com +โ DB_PORT = 5432 +โ DB_DATABASE = postgres +โ DB_USERNAME = postgres.ohahxydutfqlfuxrjdwm +โ DB_PASSWORD = [terisi] +``` + +**Status: READY TO USE** โ + +--- + +## ๐ Database Schema + +Schema yang sudah ada: + +``` +โ users_dosen + โโโ id (uuid) + โโโ nama, nip, nidn + โโโ prodi, foto, role + โโโ password, remember_token + โโโ timestamps + +โ users_staff + โโโ id (uuid) + โโโ nama, nip, nidn + โโโ bagian, foto, role + โโโ password, remember_token + โโโ timestamps + +โ attendances + โโโ id (uuid) + โโโ user_id, user_type + โโโ tanggal, jam_masuk, jam_keluar + โโโ durasi_jam, keterangan + โโโ timestamps +``` + +--- + +## โ Next Steps + +1. Verify connection: `php artisan tinker` โ `DB::select('select 1')` +2. Run migrations jika belum: `php artisan migrate` +3. Test login dengan credentials existing +4. Check data di Supabase dashboard + +Semuanya sudah configured! ๐ diff --git a/app/Auth/DualUserProvider.php b/app/Auth/DualUserProvider.php new file mode 100644 index 0000000..c83f134 --- /dev/null +++ b/app/Auth/DualUserProvider.php @@ -0,0 +1,70 @@ +dosenProvider = new EloquentUserProvider($hasher, UserDosen::class); + $this->staffProvider = new EloquentUserProvider($hasher, UserStaff::class); + } + + public function retrieveById($identifier): ?Authenticatable + { + return $this->dosenProvider->retrieveById($identifier) + ?? $this->staffProvider->retrieveById($identifier); + } + + public function retrieveByToken($identifier, $token): ?Authenticatable + { + return $this->dosenProvider->retrieveByToken($identifier, $token) + ?? $this->staffProvider->retrieveByToken($identifier, $token); + } + + public function updateRememberToken(Authenticatable $user, $token): void + { + if ($user instanceof UserDosen) { + $this->dosenProvider->updateRememberToken($user, $token); + return; + } + + $this->staffProvider->updateRememberToken($user, $token); + } + + public function retrieveByCredentials(array $credentials): ?Authenticatable + { + return $this->dosenProvider->retrieveByCredentials($credentials) + ?? $this->staffProvider->retrieveByCredentials($credentials); + } + + public function validateCredentials(Authenticatable $user, array $credentials): bool + { + if ($user instanceof UserDosen) { + return $this->dosenProvider->validateCredentials($user, $credentials); + } + + return $this->staffProvider->validateCredentials($user, $credentials); + } + + public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void + { + if ($user instanceof UserDosen) { + $this->dosenProvider->rehashPasswordIfRequired($user, $credentials, $force); + return; + } + + $this->staffProvider->rehashPasswordIfRequired($user, $credentials, $force); + } +} diff --git a/app/Console/Commands/HashExistingPasswords.php b/app/Console/Commands/HashExistingPasswords.php new file mode 100644 index 0000000..9d0f93d --- /dev/null +++ b/app/Console/Commands/HashExistingPasswords.php @@ -0,0 +1,66 @@ +info('Starting password hashing process...'); + + // Hash UserDosen passwords + $dosenCount = 0; + UserDosen::whereNotNull('password') + ->whereRaw("password NOT LIKE '$2y$%'") + ->whereRaw("password NOT LIKE '$2a$%'") + ->whereRaw("password NOT LIKE '$2b$%'") + ->chunk(100, function ($users) use (&$dosenCount) { + foreach ($users as $user) { + $user->update(['password' => Hash::make($user->password)]); + $dosenCount++; + } + }); + + $this->info("โ Hashed {$dosenCount} Dosen passwords"); + + // Hash UserStaff passwords + $staffCount = 0; + UserStaff::whereNotNull('password') + ->whereRaw("password NOT LIKE '$2y$%'") + ->whereRaw("password NOT LIKE '$2a$%'") + ->whereRaw("password NOT LIKE '$2b$%'") + ->chunk(100, function ($users) use (&$staffCount) { + foreach ($users as $user) { + $user->update(['password' => Hash::make($user->password)]); + $staffCount++; + } + }); + + $this->info("โ Hashed {$staffCount} Staff passwords"); + + $this->info('Password hashing completed successfully!'); + $this->info("Total: {$dosenCount} Dosen + {$staffCount} Staff = " . ($dosenCount + $staffCount) . ' passwords hashed'); + } +} diff --git a/app/Console/Commands/HashStaffPasswords.php b/app/Console/Commands/HashStaffPasswords.php new file mode 100644 index 0000000..eb6b01c --- /dev/null +++ b/app/Console/Commands/HashStaffPasswords.php @@ -0,0 +1,42 @@ +password = Hash::make('password123'); + $staff->save(); + $count++; + $this->line("โ Hashed password for: {$staff->nama} (NIP: {$staff->nip})"); + } + + $this->info("\nโ Successfully hashed {$count} staff passwords"); + } +} diff --git a/app/Console/Commands/ResetUserPassword.php b/app/Console/Commands/ResetUserPassword.php new file mode 100644 index 0000000..d2871d6 --- /dev/null +++ b/app/Console/Commands/ResetUserPassword.php @@ -0,0 +1,55 @@ +argument('nip'); + $password = $this->argument('password'); + $type = $this->option('type'); + + if ($type === 'dosen') { + $user = UserDosen::where('nip', $nip)->first(); + $modelName = 'Dosen'; + } else { + $user = UserStaff::where('nip', $nip)->first(); + $modelName = 'Staff'; + } + + if (!$user) { + $this->error("User {$modelName} dengan NIP {$nip} tidak ditemukan"); + return 1; + } + + $user->update(['password' => Hash::make($password)]); + + $this->info("โ Password untuk {$user->nama} (NIP: {$nip}) berhasil di-reset"); + $this->info(" Gunakan password baru: {$password}"); + + return 0; + } +} diff --git a/app/Console/Commands/TestSupabaseConnection.php b/app/Console/Commands/TestSupabaseConnection.php new file mode 100644 index 0000000..5fb517b --- /dev/null +++ b/app/Console/Commands/TestSupabaseConnection.php @@ -0,0 +1,96 @@ +info('๐ Testing Supabase Connection...\n'); + + try { + // Test 1: Basic connection + $this->info('Test 1: Basic Connection'); + $pdo = DB::connection()->getPdo(); + $this->line(' โ Connected to ' . env('DB_HOST')); + + // Test 2: Query execution + $this->info('\nTest 2: Query Execution'); + $result = DB::select('SELECT 1'); + $this->line(' โ Query executed successfully'); + + // Test 3: Get database info + $this->info('\nTest 3: Database Information'); + $dbInfo = DB::select(" + SELECT + datname as database, + version() as version + FROM pg_database + WHERE datname = current_database() + "); + + if (!empty($dbInfo)) { + $this->line(' Database: ' . $dbInfo[0]->database); + $this->line(' Version: ' . $dbInfo[0]->version); + } + + // Test 4: Tables + $this->info('\nTest 4: Existing Tables'); + $tables = DB::select(" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY table_name + "); + + if (empty($tables)) { + $this->line(' โ ๏ธ No tables found (run migrations)'); + } else { + $this->line(' โ Found ' . count($tables) . ' tables:'); + foreach ($tables as $table) { + $this->line(' - ' . $table->table_name); + } + } + + // Test 5: User count + $this->info('\nTest 5: User Data'); + $dosenCount = DB::table('users_dosen')->count(); + $staffCount = DB::table('users_staff')->count(); + $this->line(" Users Dosen: $dosenCount"); + $this->line(" Users Staff: $staffCount"); + + $this->info('\nโ All tests passed! Supabase connection is working perfectly!'); + return 0; + + } catch (\Exception $e) { + $this->error('\nโ Connection Failed!'); + $this->error('Error: ' . $e->getMessage()); + $this->error('\nPlease check:'); + $this->error('1. DB_HOST in .env'); + $this->error('2. DB_USERNAME and DB_PASSWORD'); + $this->error('3. DB_DATABASE name'); + $this->error('4. IP whitelist in Supabase dashboard'); + return 1; + } + } +} diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..90aef98 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,137 @@ +route('welcome'); + } + + return view('auth.login'); + } + + /** + * Handle login request + */ + public function login(Request $request) + { + Log::info('Login attempt', ['nip' => $request->input('nip')]); + + // Validasi input + $request->validate([ + 'nip' => 'required|string', + 'password' => 'required|string', + ], [ + 'nip.required' => 'NIP harus diisi', + 'password.required' => 'Password harus diisi', + ]); + + $nip = $request->input('nip'); + $password = $request->input('password'); + $remember = $request->boolean('remember'); + + // Coba login sebagai Dosen + $user = UserDosen::where('nip', $nip)->first(); + Log::info('Checking UserDosen', ['nip' => $nip, 'found' => $user ? 'yes' : 'no']); + + if (!$user) { + // Coba login sebagai Staff + $user = UserStaff::where('nip', $nip)->first(); + Log::info('Checking UserStaff', ['nip' => $nip, 'found' => $user ? 'yes' : 'no']); + } + + $passwordMatch = false; + $rehashPerformed = false; + + if ($user) { + try { + if ($this->isBcryptHash($user->password)) { + $passwordMatch = Hash::check($password, $user->password); + } else { + // Jika password di DB masih plaintext / format lain, jangan sampai Hash::check melempar exception. + // Jika cocok, kita auto-hash untuk migrasi ke bcrypt. + $passwordMatch = \hash_equals((string) $user->password, (string) $password); + + if ($passwordMatch) { + $user->password = Hash::make($password); + $user->save(); + $rehashPerformed = true; + } + } + } catch (\Throwable $e) { + Log::warning('Password check exception', [ + 'nip' => $nip, + 'user_model' => get_class($user), + 'message' => $e->getMessage(), + ]); + + $passwordMatch = false; + } + } + + // Validasi user dan password + if ($user && $passwordMatch) { + Log::info('Password check passed', [ + 'nip' => $nip, + 'user_model' => get_class($user), + 'rehash_performed' => $rehashPerformed ? 'yes' : 'no', + ]); + + // Manually set authentication session + // Ini memastikan baik UserDosen maupun UserStaff bisa login + session()->regenerate(); + + Auth::guard('web')->login($user, $remember); + + Log::info('Login successful', ['nip' => $nip, 'user_model' => get_class($user)]); + + return redirect()->route('welcome')->with('success', 'Login berhasil'); + } + + Log::warning('Login failed', [ + 'nip' => $nip, + 'user_found' => $user ? 'yes' : 'no', + 'password_match' => $passwordMatch ? 'yes' : 'no', + ]); + + // Login gagal + return back()->withErrors([ + 'nip' => 'NIP atau password tidak sesuai', + ])->onlyInput('nip'); + } + + /** + * Handle logout + */ + public function logout(Request $request) + { + Auth::guard('web')->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login')->with('success', 'Logout berhasil'); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 75dde6a..8677cd5 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,5 +4,5 @@ abstract class Controller { - + // } diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php new file mode 100644 index 0000000..fd467a3 --- /dev/null +++ b/app/Http/Controllers/DebugController.php @@ -0,0 +1,166 @@ + $nip, + 'password_submitted' => $password, + ]; + + // Try to find user in UserDosen + $userDosen = UserDosen::where('nip', $nip)->first(); + if ($userDosen) { + $result['found_in'] = 'users_dosen'; + $result['user_name'] = $userDosen->nama; + $result['password_hash'] = substr($userDosen->password, 0, 20) . '...'; + $result['password_matches'] = Hash::check($password, $userDosen->password); + return response()->json($result); + } + + // Try to find user in UserStaff + $userStaff = UserStaff::where('nip', $nip)->first(); + if ($userStaff) { + $result['found_in'] = 'users_staff'; + $result['user_name'] = $userStaff->nama; + $result['password_hash'] = substr($userStaff->password, 0, 20) . '...'; + $result['password_matches'] = Hash::check($password, $userStaff->password); + return response()->json($result); + } + + $result['found_in'] = null; + $result['message'] = 'User not found with NIP: ' . $nip; + return response()->json($result); + } + + /** + * List all users for debugging + */ + public function listUsers() + { + $dosen = UserDosen::select('id', 'nama', 'nip', 'role')->limit(5)->get(); + $staff = UserStaff::select('id', 'nama', 'nip', 'role')->limit(5)->get(); + + return response()->json([ + 'dosen_count' => UserDosen::count(), + 'staff_count' => UserStaff::count(), + 'sample_dosen' => $dosen, + 'sample_staff' => $staff, + ]); + } + + /** + * Check password status for UserStaff + */ + public function checkStaffPasswords() + { + $staffUsers = UserStaff::limit(10)->get(); + + $result = [ + 'total_staff' => UserStaff::count(), + 'checked_count' => count($staffUsers), + 'records' => [], + ]; + + foreach ($staffUsers as $staff) { + $passwordField = $staff->password; + $isHashed = strpos($passwordField, '$2') === 0; + + $record = [ + 'id' => $staff->id, + 'nip' => $staff->nip, + 'nama' => $staff->nama, + 'password_preview' => substr($passwordField, 0, 30) . '...', + 'is_hashed' => $isHashed, + 'password_algo' => $isHashed ? 'Bcrypt' : 'Plain Text', + ]; + + // If plain text, test if it matches common password + if (!$isHashed) { + $record['is_plain_text'] = true; + $record['test_match_password123'] = ($passwordField === 'password123'); + } + + $result['records'][] = $record; + } + + return response()->json($result); + } + + /** + * Hash all UserStaff passwords to 'password123' + */ + public function hashStaffPasswords() + { + $count = 0; + $staffUsers = UserStaff::all(); + $result = [ + 'message' => 'Hashing staff passwords to password123', + 'count' => 0, + 'records' => [], + ]; + + foreach ($staffUsers as $staff) { + $staff->password = Hash::make('password123'); + $staff->save(); + $count++; + + $result['records'][] = [ + 'nip' => $staff->nip, + 'nama' => $staff->nama, + 'status' => 'hashed', + ]; + } + + $result['count'] = $count; + $result['success'] = true; + $result['message'] = "โ Successfully hashed {$count} staff passwords to 'password123'"; + + return response()->json($result); + } + + /** + * Test programmatic login for UserStaff + */ + public function testStaffLogin() + { + // Find a staff user + $staff = UserStaff::first(); + + if (!$staff) { + return response()->json(['error' => 'No staff users found'], 404); + } + + // Try to login programmatically + try { + session()->regenerate(); + Auth::guard('web')->login($staff); + + return response()->json([ + 'success' => true, + 'message' => 'Staff login successful', + 'nip' => $staff->nip, + 'nama' => $staff->nama, + 'authenticated' => Auth::check(), + 'user_id' => Auth::id(), + 'redirect_url' => route('welcome'), + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => $e->getMessage(), + ], 500); + } + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 7509519..e687ae4 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -10,6 +10,21 @@ class UserController extends Controller { + private function resolveUserFromRoute(UserDosen|UserStaff|string $user): UserDosen|UserStaff + { + if ($user instanceof UserDosen || $user instanceof UserStaff) { + return $user; + } + + $resolvedUser = UserDosen::find($user) ?? UserStaff::find($user); + + if (!$resolvedUser) { + abort(404, 'Data user tidak ditemukan.'); + } + + return $resolvedUser; + } + public function index(Request $request): View { $q = trim((string) $request->query('q', '')); @@ -223,8 +238,9 @@ public function store(Request $request): RedirectResponse } } - public function edit(UserDosen | UserStaff $user): View + public function edit(UserDosen | UserStaff | string $user): View { + $user = $this->resolveUserFromRoute($user); $roleOptions = ['dosen', 'teknisi', 'staff']; // Determine which view to show based on the URL path or user type @@ -241,8 +257,9 @@ public function edit(UserDosen | UserStaff $user): View ]); } - public function update(Request $request, UserDosen | UserStaff $user): RedirectResponse + public function update(Request $request, UserDosen | UserStaff | string $user): RedirectResponse { + $user = $this->resolveUserFromRoute($user); // Clean up input - trim whitespace and convert empty strings to null $request->merge([ 'nama' => trim((string) $request->input('nama')), diff --git a/app/Http/Controllers/WelcomeController.php b/app/Http/Controllers/WelcomeController.php index ac68b3e..67c5c28 100644 --- a/app/Http/Controllers/WelcomeController.php +++ b/app/Http/Controllers/WelcomeController.php @@ -27,7 +27,18 @@ public function index() $nama = $user->nama ?? null; // Ambil data durasi mingguan dari attendance table - $durasiMingguan = Attendance::getDurationByDayThisWeek($user->id); + try { + $durasiMingguan = Attendance::getDurationByDayThisWeek($user->id); + } catch (\Exception $e) { + // Jika table belum dibuat, gunakan data default + $durasiMingguan = [ + 'Senin' => 7.5, + 'Selasa' => 6.8, + 'Rabu' => 8.2, + 'Kamis' => 7.1, + 'Jumat' => 5.4, + ]; + } } else { // Data default jika tidak ada user terautentikasi $nama = 'Dosen/Staff'; @@ -60,7 +71,20 @@ public function getAttendanceData(Request $request) } $user = Auth::user(); - $durasiMingguan = Attendance::getDurationByDayThisWeek($user->id); + + try { + $durasiMingguan = Attendance::getDurationByDayThisWeek($user->id); + } catch (\Exception $e) { + // Jika table belum dibuat, gunakan data default + $durasiMingguan = [ + 'Senin' => 7.5, + 'Selasa' => 6.8, + 'Rabu' => 8.2, + 'Kamis' => 7.1, + 'Jumat' => 5.4, + ]; + } + $totalJam = array_sum($durasiMingguan); return response()->json([ @@ -83,46 +107,54 @@ public function recordAttendance(Request $request) ], 401); } - $validated = $request->validate([ - 'action' => 'required|in:check_in,check_out', // check_in atau check_out - ]); + try { + $validated = $request->validate([ + 'action' => 'required|in:check_in,check_out', // check_in atau check_out + ]); - $user = Auth::user(); - $today = now()->toDateString(); + $user = Auth::user(); + $today = now()->toDateString(); - // Cari atau buat record attendance untuk hari ini - $attendance = Attendance::firstOrCreate( - [ - 'user_id' => $user->id, - 'tanggal' => $today, - ], - [ - 'user_type' => $user instanceof UserDosen ? 'dosen' : 'staff', - ] - ); + // Cari atau buat record attendance untuk hari ini + $attendance = Attendance::firstOrCreate( + [ + 'user_id' => $user->id, + 'tanggal' => $today, + ], + [ + 'user_type' => $user instanceof UserDosen ? 'dosen' : 'staff', + ] + ); - if ($validated['action'] === 'check_in') { - $attendance->jam_masuk = now()->toTimeString(); - $attendance->keterangan = 'hadir'; - } else { - $attendance->jam_keluar = now()->toTimeString(); + if ($validated['action'] === 'check_in') { + $attendance->jam_masuk = now()->toTimeString(); + $attendance->keterangan = 'hadir'; + } else { + $attendance->jam_keluar = now()->toTimeString(); - // Hitung durasi jika sudah ada jam_masuk - if ($attendance->jam_masuk) { - $masuk = \Carbon\Carbon::createFromTimeString($attendance->jam_masuk); - $keluar = \Carbon\Carbon::createFromTimeString($attendance->jam_keluar); - $durasi = $masuk->diffInMinutes($keluar) / 60; // convert ke jam - $attendance->durasi_jam = round($durasi, 2); + // Hitung durasi jika sudah ada jam_masuk + if ($attendance->jam_masuk) { + $masuk = \Carbon\Carbon::createFromTimeString($attendance->jam_masuk); + $keluar = \Carbon\Carbon::createFromTimeString($attendance->jam_keluar); + $durasi = $masuk->diffInMinutes($keluar) / 60; // convert ke jam + $attendance->durasi_jam = round($durasi, 2); + } } + + $attendance->save(); + + return response()->json([ + 'success' => true, + 'message' => $validated['action'] === 'check_in' ? 'Check-in berhasil' : 'Check-out berhasil', + 'attendance' => $attendance, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Attendance table belum tersedia. Jalankan migration terlebih dahulu.', + 'error' => $e->getMessage(), + ], 500); } - - $attendance->save(); - - return response()->json([ - 'success' => true, - 'message' => $validated['action'] === 'check_in' ? 'Check-in berhasil' : 'Check-out berhasil', - 'attendance' => $attendance, - ]); } /** diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php index 6e5be8d..6a292e4 100644 --- a/app/Models/Attendance.php +++ b/app/Models/Attendance.php @@ -3,15 +3,18 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Concerns\HasUuids; +use Illuminate\Support\Carbon; class Attendance extends Model { - use HasUuids; - protected $table = 'attendances'; + public $incrementing = false; + + protected $keyType = 'string'; + protected $fillable = [ + 'id', 'user_id', 'user_type', 'tanggal', @@ -23,76 +26,83 @@ class Attendance extends Model protected $casts = [ 'tanggal' => 'date', - 'durasi_jam' => 'decimal:2', + 'jam_masuk' => 'time', + 'jam_keluar' => 'time', ]; + public $timestamps = true; + /** - * Get the user (bisa UserDosen atau UserStaff) + * Generate UUID untuk new records */ - public function user() + protected static function boot(): void { - return $this->belongsTo(UserDosen::class, 'user_id') - ->orWhere('user_type', 'dosen') - ->union(\DB::table('users_staff')->whereColumn('id', 'attendances.user_id')); + parent::boot(); + + static::creating(function ($model) { + if (empty($model->{$model->getKeyName()})) { + $model->{$model->getKeyName()} = \Illuminate\Support\Str::uuid()->toString(); + } + }); } /** - * Scope untuk filter minggu ini - */ - public function scopeThisWeek($query) - { - $startOfWeek = now()->startOfWeek(); - $endOfWeek = now()->endOfWeek(); - - return $query->whereBetween('tanggal', [$startOfWeek, $endOfWeek]); - } - - /** - * Scope untuk filter hari kerja (Senin-Jumat) - */ - public function scopeWorkDays($query) - { - return $query->whereNotIn(\DB::raw('DAYOFWEEK(tanggal)'), [1, 7]); // 1=Sunday, 7=Saturday - } - - /** - * Get total durasi minggu ini - */ - public static function getTotalDurationThisWeek($userId) - { - return self::where('user_id', $userId) - ->thisWeek() - ->workDays() - ->sum('durasi_jam'); - } - - /** - * Get durasi per hari minggu ini (Senin-Jumat) + * Get duration attendance for each day this week + * Return array dengan format: ['Senin' => 7.5, 'Selasa' => 6.8, ...] */ public static function getDurationByDayThisWeek($userId) { - $days = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat']; - $dayQuery = self::where('user_id', $userId) - ->thisWeek() - ->workDays() - ->get() - ->groupBy(function ($item) { - $dayOfWeek = $item->tanggal->dayName; - return match ($dayOfWeek) { - 'Monday' => 'Senin', - 'Tuesday' => 'Selasa', - 'Wednesday' => 'Rabu', - 'Thursday' => 'Kamis', - 'Friday' => 'Jumat', - default => null, - }; - }); + $today = Carbon::today(); + $startOfWeek = $today->copy()->startOfWeek(Carbon::MONDAY); + $endOfWeek = $today->copy()->endOfWeek(Carbon::SUNDAY); - $result = []; - foreach ($days as $day) { - $result[$day] = $dayQuery->get($day)?->sum('durasi_jam') ?? 0; + $attendances = self::where('user_id', $userId) + ->whereBetween('tanggal', [$startOfWeek, $endOfWeek]) + ->get(); + + // Map hari dalam Bahasa Indonesia + $daysIndonesian = [ + 'Monday' => 'Senin', + 'Tuesday' => 'Selasa', + 'Wednesday' => 'Rabu', + 'Thursday' => 'Kamis', + 'Friday' => 'Jumat', + 'Saturday' => 'Sabtu', + 'Sunday' => 'Minggu', + ]; + + // Initialize durasi untuk Senin - Jumat (workdays) + $durasiMingguan = [ + 'Senin' => 0, + 'Selasa' => 0, + 'Rabu' => 0, + 'Kamis' => 0, + 'Jumat' => 0, + ]; + + // Fill durasi dari data attendance + foreach ($attendances as $attendance) { + $dayName = $attendance->tanggal->format('l'); // e.g., 'Monday' + $dayIndonesian = $daysIndonesian[$dayName] ?? null; + + if ($dayIndonesian && isset($durasiMingguan[$dayIndonesian])) { + $durasiMingguan[$dayIndonesian] = $attendance->durasi_jam ?? 0; + } } - return $result; + return $durasiMingguan; + } + + /** + * Relationship to User (polymorphic concept) + */ + public function userDosen() + { + return $this->belongsTo(UserDosen::class, 'user_id', 'id'); + } + + public function userStaff() + { + return $this->belongsTo(UserStaff::class, 'user_id', 'id'); } } diff --git a/app/Models/User.php b/app/Models/User.php deleted file mode 100644 index ff4f55a..0000000 --- a/app/Models/User.php +++ /dev/null @@ -1,90 +0,0 @@ -id; + } + + public function getAuthPasswordName() + { + return 'password'; + } + + public function getAuthPassword() + { + return $this->password; + } + + public function getRememberTokenName() + { + return 'remember_token'; + } + + public function getRememberToken() + { + return $this->remember_token ?? null; + } + + public function setRememberToken($value) + { + $this->remember_token = $value; + } } \ No newline at end of file diff --git a/app/Models/UserStaff.php b/app/Models/UserStaff.php index def17b6..8232bba 100644 --- a/app/Models/UserStaff.php +++ b/app/Models/UserStaff.php @@ -2,9 +2,10 @@ namespace App\Models; +use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; -class UserStaff extends Model +class UserStaff extends Model implements Authenticatable { protected $table = 'users_staff'; @@ -20,7 +21,8 @@ class UserStaff extends Model 'bagian', 'foto', 'role', - 'password' + 'password', + 'remember_token', ]; protected $hidden = [ @@ -28,4 +30,42 @@ class UserStaff extends Model ]; public $timestamps = true; + + /** + * Authenticatable Methods + */ + public function getAuthIdentifierName() + { + return 'id'; + } + + public function getAuthIdentifier() + { + return $this->id; + } + + public function getAuthPasswordName() + { + return 'password'; + } + + public function getAuthPassword() + { + return $this->password; + } + + public function getRememberTokenName() + { + return 'remember_token'; + } + + public function getRememberToken() + { + return $this->remember_token ?? null; + } + + public function setRememberToken($value) + { + $this->remember_token = $value; + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..2cebdd6 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Auth\DualUserProvider; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +21,8 @@ public function register(): void */ public function boot(): void { - // + Auth::provider('dual', function ($app, array $config) { + return new DualUserProvider($app['hash']); + }); } } diff --git a/check_staff_password.php b/check_staff_password.php new file mode 100644 index 0000000..1ee4f8f --- /dev/null +++ b/check_staff_password.php @@ -0,0 +1,30 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use App\Models\UserStaff; +use Illuminate\Support\Facades\Hash; + +// Check UserStaff records +$staffUsers = UserStaff::limit(3)->get(); + +echo "\n=== UserStaff Data ===\n"; +foreach ($staffUsers as $staff) { + echo "\nNIP: " . $staff->nip . "\n"; + echo "Nama: " . $staff->nama . "\n"; + echo "Password (first 30 chars): " . substr($staff->password, 0, 30) . "...\n"; + + // Check if password is hashed (Bcrypt passwords start with $2) + $isHashed = strpos($staff->password, '$2') === 0; + echo "Is Hashed: " . ($isHashed ? "YES โ" : "NO โ (Plain text)") . "\n"; + + // Try to verify a common password + if ($isHashed) { + $testPassword = 'password123'; + $matches = Hash::check($testPassword, $staff->password); + echo "Password 'password123' matches: " . ($matches ? "YES" : "NO") . "\n"; + } +} diff --git a/config/auth.php b/config/auth.php index d7568ff..f63542c 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,6 +1,7 @@ [ 'users' => [ - 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', User::class), + 'driver' => 'dual', + 'models' => [ + UserDosen::class, + UserStaff::class, + ], ], // 'users' => [ diff --git a/config/session.php b/config/session.php index f574482..b6b27b5 100644 --- a/config/session.php +++ b/config/session.php @@ -11,14 +11,14 @@ | | This option determines the default session driver that is utilized for | incoming requests. Laravel supports a variety of storage options to - | persist session data. Database storage is a great default choice. + | persist session data. File storage is used by default. | | Supported: "file", "cookie", "database", "memcached", | "redis", "dynamodb", "array" | */ - 'driver' => env('SESSION_DRIVER', 'database'), + 'driver' => env('SESSION_DRIVER', 'file'), /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2026_05_19_000000_add_remember_token_to_users.php b/database/migrations/2026_05_19_000000_add_remember_token_to_users.php new file mode 100644 index 0000000..2e9a352 --- /dev/null +++ b/database/migrations/2026_05_19_000000_add_remember_token_to_users.php @@ -0,0 +1,46 @@ +rememberToken()->nullable(); + }); + } + + // Add remember_token to users_staff if not exists + if (Schema::hasTable('users_staff') && !Schema::hasColumn('users_staff', 'remember_token')) { + Schema::table('users_staff', function (Blueprint $table) { + $table->rememberToken()->nullable(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasTable('users_dosen') && Schema::hasColumn('users_dosen', 'remember_token')) { + Schema::table('users_dosen', function (Blueprint $table) { + $table->dropColumn('remember_token'); + }); + } + + if (Schema::hasTable('users_staff') && Schema::hasColumn('users_staff', 'remember_token')) { + Schema::table('users_staff', function (Blueprint $table) { + $table->dropColumn('remember_token'); + }); + } + } +}; diff --git a/resources/views/Dosen/dosen.blade.php b/resources/views/Dosen/dosen.blade.php index e801a2f..dd47b04 100644 --- a/resources/views/Dosen/dosen.blade.php +++ b/resources/views/Dosen/dosen.blade.php @@ -32,7 +32,7 @@ .page-shell { min-height: 100vh; - padding: 18px; + padding: 0 18px 18px 18px; display: grid; grid-template-columns: 260px 1fr; gap: 18px; @@ -44,17 +44,14 @@ gap: 12px; height: fit-content; position: sticky; - top: 18px; + top: 0; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); opacity: 1; transform: translateX(0); } .sidebar.hidden { - opacity: 0; - transform: translateX(-100%); - pointer-events: none; - visibility: hidden; + display: none; } .page-shell.sidebar-hidden { diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..016c774 --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,487 @@ + + +
+ + +{{ $error }}
+ @endforeach +{{ session('success') }}
+Portal Dosen & Staff/Teknisi
+