diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 930fda7..4f2a6f3 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -126,6 +126,26 @@ class UserController extends Controller ->with('success', 'Password untuk ' . $user->name . ' berhasil direset.'); } + /** + * Akhiri sesi aktif user (single session) — user harus login ulang. + */ + public function forceLogout(User $user): RedirectResponse + { + if ($user->id === auth()->id()) { + return back()->with('error', 'Tidak bisa memaksa logout diri sendiri.'); + } + + $token = $user->active_session_token; + $user->forceFill(['active_session_token' => null])->save(); + + // Hapus session file lama agar perangkat lama benar-benar terlempar + if ($token && config('session.driver') === 'file') { + @unlink(storage_path('framework/sessions/'.$token)); + } + + return back()->with('success', 'Sesi '.$user->name.' diakhiri — akun bisa login kembali.'); + } + /** * Kirim akun (link + username + password baru) ke nomor HP user via WhatsApp. * Password lama tidak bisa dikirim (tersimpan hash) — jadi dibuatkan password baru. diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 8d25fac..306052d 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -23,10 +23,22 @@ class LoginController extends Controller ]); if (Auth::attempt($credentials, $request->boolean('remember'))) { + $user = Auth::user(); + + // Single session: tolak login bila akun masih aktif di perangkat lain + if ($user->sessionTokenIsAlive()) { + Auth::logout(); + + return back()->withErrors([ + 'username' => 'Akun ini sedang digunakan di perangkat lain. Silakan logout dari perangkat tersebut terlebih dahulu.', + ])->onlyInput('username'); + } + $request->session()->regenerate(); + $user->forceFill(['active_session_token' => $request->session()->getId()])->save(); // Role 'other' langsung ke presensi, bukan dashboard - if (Auth::user()->role === 'other') { + if ($user->role === 'other') { return redirect()->route('attendances.index'); } @@ -40,6 +52,12 @@ class LoginController extends Controller public function logout(Request $request): RedirectResponse { + $user = Auth::user(); + + if ($user && $user->active_session_token === $request->session()->getId()) { + $user->forceFill(['active_session_token' => null])->save(); + } + Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); diff --git a/app/Http/Middleware/SingleSession.php b/app/Http/Middleware/SingleSession.php new file mode 100644 index 0000000..5d65ec5 --- /dev/null +++ b/app/Http/Middleware/SingleSession.php @@ -0,0 +1,41 @@ +active_session_token; + + if ($token && $token !== Session::getId()) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login') + ->withErrors(['username' => 'Sesi berakhir: akun ini sedang digunakan di perangkat lain.']) + ->onlyInput('username'); + } + + return $next($request); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 3054c00..07ebb5f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -13,7 +13,7 @@ class User extends Authenticatable public const ROLES = ['admin', 'bk', 'wali_kelas', 'staff', 'other', 'kepala_sekolah', 'waka_kesiswaan', 'ketua_tim']; protected $fillable = [ - 'name', 'username', 'email', 'phone', 'password', 'role', 'roles', 'is_active', + 'name', 'username', 'email', 'phone', 'password', 'role', 'roles', 'is_active', 'active_session_token', ]; protected $hidden = ['password', 'remember_token']; @@ -150,4 +150,24 @@ class User extends Authenticatable { return $this->hasMany(ViolationHandling::class, 'created_by'); } + + /** + * Cek apakah token sesi aktif masih "hidup" (session file/DB masih ada). + * Driver array/lain → dianggap hidup (aman, tolak login ganda). + */ + public function sessionTokenIsAlive(): bool + { + $token = $this->active_session_token; + if (! $token) { + return false; + } + + return match (config('session.driver')) { + 'file' => file_exists(storage_path('framework/sessions/'.$token)), + 'database' => \Illuminate\Support\Facades\DB::table(config('session.table', 'sessions')) + ->where('id', $token) + ->exists(), + default => true, + }; + } } diff --git a/bootstrap/app.php b/bootstrap/app.php index cc4d5f0..638d6fe 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -23,6 +23,9 @@ return Application::configure(basePath: dirname(__DIR__)) 'role' => \App\Http\Middleware\CheckRole::class, 'permission' => \App\Http\Middleware\PermissionMiddleware::class, ]); + $middleware->web(append: [ + \App\Http\Middleware\SingleSession::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/database/migrations/2026_08_03_102109_add_active_session_token_to_users.php b/database/migrations/2026_08_03_102109_add_active_session_token_to_users.php new file mode 100644 index 0000000..742299d --- /dev/null +++ b/database/migrations/2026_08_03_102109_add_active_session_token_to_users.php @@ -0,0 +1,28 @@ +string('active_session_token', 191)->nullable()->after('phone'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('active_session_token'); + }); + } +}; diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index 2c7fb89..fb636dd 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -131,6 +131,17 @@ @if($user->id !== auth()->id()) +
+ @endif