From af05a39a821ceeaddfd0135344401609ff49ba27 Mon Sep 17 00:00:00 2001 From: arifal hidayat Date: Thu, 7 Aug 2025 00:51:46 +0700 Subject: [PATCH] fix menu tax in data and fix session when multiple user login --- app/Http/Controllers/Api/UsersController.php | 4 +- .../Auth/AuthenticatedSessionController.php | 16 +- .../Middleware/ValidateApiTokenForWeb.php | 167 +++++++++ bootstrap/app.php | 3 + database/seeders/MenuSeeder.php | 25 +- database/seeders/UsersRoleMenuSeeder.php | 11 +- public/js/utils/simple-session-validator.js | 233 ++++++++++++ resources/js/utils/api-token-manager.js | 52 ++- resources/js/utils/api-token-web-validator.js | 337 ++++++++++++++++++ .../js/utils/multi-user-session-handler.js | 264 ++++++++++++++ resources/js/utils/session-manager.js | 126 +++++++ .../layouts/partials/vendor-scripts.blade.php | 5 + routes/web.php | 2 +- 13 files changed, 1209 insertions(+), 36 deletions(-) create mode 100644 app/Http/Middleware/ValidateApiTokenForWeb.php create mode 100644 public/js/utils/simple-session-validator.js create mode 100644 resources/js/utils/api-token-web-validator.js create mode 100644 resources/js/utils/multi-user-session-handler.js create mode 100644 resources/js/utils/session-manager.js diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 1ea9d9b..4dc2ea4 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -36,7 +36,9 @@ class UsersController extends Controller return UserResource::collection($query->paginate(config('app.paginate_per_page', 50))); } public function logout(Request $request){ - $request->user()->tokens()->delete(); + \Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $request->user()->id) + ->where('tokenable_type', get_class($request->user())) + ->delete(); return response()->json(['message' => 'logged out successfully']); } public function store(UsersRequest $request){ diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 748ee64..05d4d87 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -37,7 +37,9 @@ class AuthenticatedSessionController extends Controller $user = Auth::user(); // Hapus token lama jika ada - $user->tokens()->delete(); + \Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id) + ->where('tokenable_type', get_class($user)) + ->delete(); // Buat token untuk API dengan scope dan expiration $tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time(); @@ -47,6 +49,10 @@ class AuthenticatedSessionController extends Controller // Simpan token di session untuk digunakan di frontend session(['api_token' => $token]); + + // Simpan timestamp login untuk validasi multi-user + session(['login_timestamp' => now()->timestamp]); + session(['user_id' => $user->id]); return redirect()->intended(RouteServiceProvider::HOME); } @@ -66,7 +72,9 @@ class AuthenticatedSessionController extends Controller } // Delete existing tokens - $user->tokens()->delete(); + \Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id) + ->where('tokenable_type', get_class($user)) + ->delete(); // Generate new token $tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time(); @@ -107,7 +115,9 @@ class AuthenticatedSessionController extends Controller public function destroy(Request $request) { if($request->user()){ - $request->user()->tokens()->delete(); + \Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $request->user()->id) + ->where('tokenable_type', get_class($request->user())) + ->delete(); } Auth::guard('web')->logout(); diff --git a/app/Http/Middleware/ValidateApiTokenForWeb.php b/app/Http/Middleware/ValidateApiTokenForWeb.php new file mode 100644 index 0000000..02e9b96 --- /dev/null +++ b/app/Http/Middleware/ValidateApiTokenForWeb.php @@ -0,0 +1,167 @@ +is('api/*')) { + return $next($request); + } + + $user = Auth::user(); + $sessionToken = Session::get('api_token'); + + // Jika tidak ada token di session, generate token baru + if (!$sessionToken) { + $this->generateNewToken($user); + return $next($request); + } + + // Validasi token API + if (!$this->isTokenValid($sessionToken, $user)) { + // Token invalid, check apakah ada user lain yang login + if ($this->hasOtherUserLoggedIn($user)) { + // User lain sudah login, force logout user ini + $this->forceLogout($request, 'User lain telah login. Silakan login ulang.'); + return $this->redirectToLogin($request, 'User lain telah login. Silakan login ulang.'); + } else { + // Generate token baru jika tidak ada user lain + $this->generateNewToken($user); + } + } + + return $next($request); + } + + /** + * Check apakah token API masih valid + */ + private function isTokenValid($sessionToken, $user): bool + { + if (!$sessionToken || !$user) { + return false; + } + + // Extract plain token dari session token + $tokenParts = explode('|', $sessionToken); + if (count($tokenParts) !== 2) { + return false; + } + + $plainToken = $tokenParts[1]; + + // Check token di database + $validToken = PersonalAccessToken::where('tokenable_id', $user->id) + ->where('tokenable_type', get_class($user)) + ->where('token', hash('sha256', $plainToken)) + ->where(function($query) { + $query->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->first(); + + return $validToken !== null; + } + + /** + * Check apakah ada user lain yang login (token baru dibuat) + */ + private function hasOtherUserLoggedIn($currentUser): bool + { + $sessionUserId = Session::get('user_id'); + + // Jika ada user_id di session tapi tidak match dengan current user + if ($sessionUserId && $sessionUserId != $currentUser->id) { + return true; + } + + // Check apakah ada token aktif lain untuk user ini + $activeTokens = PersonalAccessToken::where('tokenable_id', $currentUser->id) + ->where('tokenable_type', get_class($currentUser)) + ->where(function($query) { + $query->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->count(); + + // Jika tidak ada token aktif, kemungkinan user lain sudah login + return $activeTokens === 0; + } + + /** + * Generate token baru untuk user + */ + private function generateNewToken($user): void + { + // Hapus token lama + PersonalAccessToken::where('tokenable_id', $user->id) + ->where('tokenable_type', get_class($user)) + ->delete(); + + // Generate token baru + $tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time(); + $token = $user->createToken($tokenName, ['*'], now()->addDays(30))->plainTextToken; + + // Simpan token di session + Session::put('api_token', $token); + Session::put('user_id', $user->id); + Session::put('login_timestamp', now()->timestamp); + } + + /** + * Force logout user dan clear semua sessions + */ + private function forceLogout(Request $request, string $reason = 'Session tidak valid'): void + { + $user = Auth::user(); + + if ($user) { + // Delete all tokens for this user + PersonalAccessToken::where('tokenable_id', $user->id) + ->where('tokenable_type', get_class($user)) + ->delete(); + } + + // Clear session + Session::forget(['api_token', 'user_id', 'login_timestamp']); + Auth::guard('web')->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + } + + /** + * Redirect ke login dengan pesan error + */ + private function redirectToLogin(Request $request, string $message): Response + { + if ($request->expectsJson() || $request->ajax()) { + return response()->json([ + 'error' => $message, + 'redirect' => route('login'), + 'force_logout' => true + ], 401); + } + + return redirect()->route('login')->with('error', $message); + } +} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index 3bb9d6c..460203c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware) { + $middleware->alias([ + 'validate.api.token.web' => \App\Http\Middleware\ValidateApiTokenForWeb::class, + ]); }) ->withExceptions(function (Exceptions $exceptions) { $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){ diff --git a/database/seeders/MenuSeeder.php b/database/seeders/MenuSeeder.php index b6d1dcb..317db38 100644 --- a/database/seeders/MenuSeeder.php +++ b/database/seeders/MenuSeeder.php @@ -14,6 +14,8 @@ class MenuSeeder extends Seeder */ public function run(): void { + Menu::whereIn('name', ['Data Pajak','Pajak'])->delete(); + $menus = [ [ "name" => "Neng Bedas", @@ -193,6 +195,12 @@ class MenuSeeder extends Seeder "icon" => null, "sort_order" => 9, ], + [ + "name" => "Pajak", + "url" => "taxation", + "icon" => null, + "sort_order" => 10, + ] ] ], [ @@ -270,21 +278,6 @@ class MenuSeeder extends Seeder ], ] ], - [ - "name" => "Pajak", - "url" => "/tax", - "icon" => "mingcute:coin-line", - "parent_id" => null, - "sort_order" => 10, - "children" => [ - [ - "name" => "Data Pajak", - "url" => "taxation", - "icon" => null, - "sort_order" => 1, - ] - ] - ] ]; foreach ($menus as $menuData) { @@ -292,7 +285,7 @@ class MenuSeeder extends Seeder } } - private function createOrUpdateMenu($menuData, $parentId = null){ + private function createOrUpdateMenu($menuData, $parentId = null){ $menuData['parent_id'] = $parentId; $menu = Menu::updateOrCreate(['name' => $menuData['name']], Arr::except($menuData, ['children'])); diff --git a/database/seeders/UsersRoleMenuSeeder.php b/database/seeders/UsersRoleMenuSeeder.php index 40097d0..7181bf5 100644 --- a/database/seeders/UsersRoleMenuSeeder.php +++ b/database/seeders/UsersRoleMenuSeeder.php @@ -24,9 +24,8 @@ class UsersRoleMenuSeeder extends Seeder 'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize', 'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata', 'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA', - 'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT', - 'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan', - 'Pajak', 'Data Pajak' + 'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT', 'Pajak', + 'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan' ])->get()->keyBy('name'); // Define access levels for each role @@ -37,7 +36,7 @@ class UsersRoleMenuSeeder extends Seeder 'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata', 'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem', 'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat', - 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan' + 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan', 'Pajak' ], 'user' => ['Dashboard', 'Data', 'Laporan', 'Neng Bedas', 'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize', @@ -68,10 +67,10 @@ class UsersRoleMenuSeeder extends Seeder // Attach User to role super admin $accountSuperadmin = User::where('email', 'superadmin@sibedas.com')->first(); + $accountDevelopment = User::where('email', 'development@sibedas.com')->first(); $accountUser = User::where('email', 'user@sibedas.com')->first(); - // $accountDefault = User::where('email','user@demo.com')->first(); $accountSuperadmin->roles()->sync([$roles['superadmin']->id]); + $accountDevelopment->roles()->sync([$roles['superadmin']->id]); $accountUser->roles()->sync([$roles['user']->id]); - // $accountDefault->roles()->sync([$roles['user']->id]); } } diff --git a/public/js/utils/simple-session-validator.js b/public/js/utils/simple-session-validator.js new file mode 100644 index 0000000..95c66b6 --- /dev/null +++ b/public/js/utils/simple-session-validator.js @@ -0,0 +1,233 @@ +/** + * Simple Session Validator + * Menangani validasi session tanpa periodic checking + * Hanya respond pada 401 errors dari API requests + */ +class SimpleSessionValidator { + constructor() { + this.isLoggingOut = false; + this.consecutiveErrors = 0; + this.maxConsecutiveErrors = 2; + this.init(); + } + + init() { + console.log("Simple Session Validator initialized"); + + // Intercept all AJAX requests untuk detect 401 + this.interceptAjaxRequests(); + + // Listen untuk page visibility changes + document.addEventListener("visibilitychange", () => { + if (!document.hidden && this.consecutiveErrors > 0) { + // Reset errors ketika user kembali ke tab + this.consecutiveErrors = 0; + console.log("Page visible, reset error counter"); + } + }); + } + + interceptAjaxRequests() { + const validator = this; + + // Intercept fetch requests + const originalFetch = window.fetch; + window.fetch = async function (...args) { + try { + const response = await originalFetch(...args); + + // Check if response is 401 dan URL mengandung /api/ + if (response.status === 401) { + const url = args[0]; + if (typeof url === "string" && url.includes("/api/")) { + console.log("401 detected in API fetch request:", url); + validator.handleApiError401(url); + } + } + + return response; + } catch (error) { + console.error("Fetch request failed:", error); + throw error; + } + }; + + // Intercept XMLHttpRequest + const originalXHRSend = XMLHttpRequest.prototype.send; + const originalXHROpen = XMLHttpRequest.prototype.open; + + XMLHttpRequest.prototype.open = function (...args) { + this._url = args[1]; + return originalXHROpen.apply(this, args); + }; + + XMLHttpRequest.prototype.send = function (...args) { + const xhr = this; + const originalOnReadyStateChange = xhr.onreadystatechange; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 401) { + if (xhr._url && xhr._url.includes("/api/")) { + console.log( + "401 detected in API XHR request:", + xhr._url + ); + validator.handleApiError401(xhr._url); + } + } + + if (originalOnReadyStateChange) { + originalOnReadyStateChange.apply(xhr, arguments); + } + }; + + return originalXHRSend.apply(this, args); + }; + + // Intercept jQuery AJAX jika tersedia + if (typeof $ !== "undefined" && $.ajaxSetup) { + $(document).ajaxError(function (event, xhr, settings, thrownError) { + if ( + xhr.status === 401 && + settings.url && + settings.url.includes("/api/") + ) { + console.log( + "401 detected in jQuery AJAX request:", + settings.url + ); + validator.handleApiError401(settings.url); + } + }); + } + } + + handleApiError401(url) { + if (this.isLoggingOut) { + return; + } + + console.log(`API 401 Error detected on ${url}`); + + // Increment consecutive errors + this.consecutiveErrors++; + + // Jika sudah 2x error berturut-turut, logout + if (this.consecutiveErrors >= this.maxConsecutiveErrors) { + this.handleSessionInvalid( + "Token API tidak valid. User lain telah login." + ); + } + } + + handleSessionInvalid(message) { + if (this.isLoggingOut) { + return; + } + + this.isLoggingOut = true; + console.log("Handling session invalid:", message); + + // Show notification + this.showNotification(message, "warning"); + + // Redirect to login after 3 seconds + setTimeout(() => { + this.forceLogout(); + }, 3000); + } + + showNotification(message, type = "info") { + // Try different notification libraries + if (typeof toastr !== "undefined") { + toastr[type](message); + } else if (typeof Swal !== "undefined") { + Swal.fire({ + title: "Peringatan Session", + text: message, + icon: type, + confirmButtonText: "OK", + allowOutsideClick: false, + timer: 5000, + timerProgressBar: true, + }); + } else { + // Create custom notification + this.createCustomNotification(message, type); + } + } + + createCustomNotification(message, type) { + const notification = document.createElement("div"); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === "warning" ? "#ffc107" : "#007bff"}; + color: white; + padding: 15px 20px; + border-radius: 5px; + z-index: 9999; + max-width: 400px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + font-family: Arial, sans-serif; + font-size: 14px; + `; + notification.innerHTML = ` + Peringatan!
+ ${message}
+ Anda akan diarahkan ke halaman login... + `; + + document.body.appendChild(notification); + + // Remove after 8 seconds + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 8000); + } + + forceLogout() { + console.log("Forcing logout..."); + + // Try to logout via API first + fetch("/logout", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": + document + .querySelector('meta[name="csrf-token"]') + ?.getAttribute("content") || "", + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "include", + }) + .then(() => { + window.location.href = "/login"; + }) + .catch(() => { + // Force redirect even if logout fails + window.location.href = "/login"; + }); + } + + // Method untuk manual reset + reset() { + this.consecutiveErrors = 0; + this.isLoggingOut = false; + console.log("Session validator reset"); + } +} + +// Initialize when DOM is ready +document.addEventListener("DOMContentLoaded", () => { + window.simpleSessionValidator = new SimpleSessionValidator(); +}); + +// Export for module usage +if (typeof module !== "undefined" && module.exports) { + module.exports = SimpleSessionValidator; +} diff --git a/resources/js/utils/api-token-manager.js b/resources/js/utils/api-token-manager.js index c5cb0fb..1f22993 100644 --- a/resources/js/utils/api-token-manager.js +++ b/resources/js/utils/api-token-manager.js @@ -171,16 +171,23 @@ class ApiTokenManager { try { const response = await fetch(url, mergedOptions); - // If unauthorized, try to generate new token - if (response.status === 401 && this.hasToken()) { - console.log('Token expired, generating new token...'); - const newToken = await this.generateToken(); - - if (newToken) { - // Retry request with new token - mergedOptions.headers.Authorization = `Bearer ${newToken}`; - return fetch(url, mergedOptions); + // If unauthorized, check if it's a session issue + if (response.status === 401) { + if (this.hasToken()) { + console.log('Token expired, generating new token...'); + const newToken = await this.generateToken(); + + if (newToken) { + // Retry request with new token + mergedOptions.headers.Authorization = `Bearer ${newToken}`; + return fetch(url, mergedOptions); + } } + + // If still 401, it might be a session issue + console.log('Session invalid, redirecting to login...'); + this.handleSessionInvalid(); + return response; } return response; @@ -189,6 +196,33 @@ class ApiTokenManager { throw error; } } + + handleSessionInvalid() { + // Show notification + this.showNotification('Session Anda telah berakhir. Silakan login ulang.', 'warning'); + + // Redirect to login after 3 seconds + setTimeout(() => { + window.location.href = '/login'; + }, 3000); + } + + showNotification(message, type = 'info') { + // Check if notification library exists + if (typeof toastr !== 'undefined') { + toastr[type](message); + } else if (typeof Swal !== 'undefined') { + Swal.fire({ + title: 'Peringatan', + text: message, + icon: type, + confirmButtonText: 'OK' + }); + } else { + // Fallback to alert + alert(message); + } + } } // Export singleton instance diff --git a/resources/js/utils/api-token-web-validator.js b/resources/js/utils/api-token-web-validator.js new file mode 100644 index 0000000..c3da00a --- /dev/null +++ b/resources/js/utils/api-token-web-validator.js @@ -0,0 +1,337 @@ +/** + * API Token Web Validator + * Menangani validasi token API untuk web requests dan auto-logout + */ +class ApiTokenWebValidator { + constructor() { + this.isLoggingOut = false; + this.checkInterval = null; + this.lastCheckTime = 0; + this.consecutiveErrors = 0; + this.maxConsecutiveErrors = 3; + this.init(); + } + + init() { + console.log("API Token Web Validator initialized"); + + // Start periodic validation + this.startPeriodicValidation(); + + // Intercept all AJAX requests + this.interceptAjaxRequests(); + + // Listen for page visibility changes + document.addEventListener("visibilitychange", () => { + if (!document.hidden) { + this.validateToken(); + } + }); + + // Listen for window focus + window.addEventListener("focus", () => { + this.validateToken(); + }); + } + + startPeriodicValidation() { + // Check token validity every 15 seconds + this.checkInterval = setInterval(() => { + this.validateToken(); + }, 15000); + } + + stopPeriodicValidation() { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + } + + async validateToken() { + // Prevent multiple simultaneous checks + if (this.isLoggingOut) { + return; + } + + // Prevent checking too frequently + const now = Date.now(); + if (now - this.lastCheckTime < 3000) { + return; + } + + this.lastCheckTime = now; + + try { + const response = await fetch("/api/check-session", { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + "Cache-Control": "no-cache", + }, + credentials: "include", + }); + + if (response.status === 401) { + console.log("Token validation failed: 401 Unauthorized"); + this.consecutiveErrors++; + + if (this.consecutiveErrors >= this.maxConsecutiveErrors) { + this.handleTokenInvalid( + "Token API tidak valid. User lain mungkin telah login." + ); + } + } else if (response.status === 200) { + // Reset consecutive errors on successful response + this.consecutiveErrors = 0; + + const data = await response.json(); + if (!data.valid) { + console.log("Token validation failed: Session invalid"); + this.handleTokenInvalid( + "Session tidak valid. Silakan login ulang." + ); + } + } + } catch (error) { + console.error("Token validation error:", error); + this.consecutiveErrors++; + + if (this.consecutiveErrors >= this.maxConsecutiveErrors) { + this.handleTokenInvalid( + "Terjadi kesalahan koneksi. Silakan login ulang." + ); + } + } + } + + interceptAjaxRequests() { + // Intercept fetch requests + const originalFetch = window.fetch; + const validator = this; + + window.fetch = async function (...args) { + try { + const response = await originalFetch(...args); + + // Check if response is 401 and it's an API call + if (response.status === 401) { + const url = args[0]; + if (typeof url === "string" && url.includes("/api/")) { + console.log("401 detected in API fetch request:", url); + validator.handleApiError401(response, url); + } + } + + return response; + } catch (error) { + console.error("Fetch request failed:", error); + throw error; + } + }; + + // Intercept XMLHttpRequest + const originalXHRSend = XMLHttpRequest.prototype.send; + const originalXHROpen = XMLHttpRequest.prototype.open; + + XMLHttpRequest.prototype.open = function (...args) { + this._url = args[1]; + return originalXHROpen.apply(this, args); + }; + + XMLHttpRequest.prototype.send = function (...args) { + const xhr = this; + const originalOnReadyStateChange = xhr.onreadystatechange; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 401) { + if (xhr._url && xhr._url.includes("/api/")) { + console.log( + "401 detected in API XHR request:", + xhr._url + ); + validator.handleApiError401(null, xhr._url); + } + } + + if (originalOnReadyStateChange) { + originalOnReadyStateChange.apply(xhr, arguments); + } + }; + + return originalXHRSend.apply(this, args); + }; + + // Intercept jQuery AJAX if available + if (typeof $ !== "undefined" && $.ajaxSetup) { + $(document).ajaxError(function (event, xhr, settings, thrownError) { + if ( + xhr.status === 401 && + settings.url && + settings.url.includes("/api/") + ) { + console.log( + "401 detected in jQuery AJAX request:", + settings.url + ); + validator.handleApiError401(null, settings.url); + } + }); + } + } + + handleApiError401(response, url) { + if (this.isLoggingOut) { + return; + } + + console.log(`API 401 Error detected on ${url}`); + + // Increment consecutive errors + this.consecutiveErrors++; + + // If we get multiple 401s, likely token is invalid + if (this.consecutiveErrors >= 2) { + this.handleTokenInvalid( + "Token API tidak valid. User lain telah login atau session berakhir." + ); + } + } + + handleTokenInvalid(message) { + if (this.isLoggingOut) { + return; + } + + this.isLoggingOut = true; + this.stopPeriodicValidation(); + + console.log("Handling token invalid:", message); + + // Show notification + this.showNotification(message, "warning"); + + // Clear any stored data + this.clearStoredData(); + + // Redirect to login after 3 seconds + setTimeout(() => { + this.forceLogout(); + }, 3000); + } + + showNotification(message, type = "info") { + // Try different notification libraries + if (typeof toastr !== "undefined") { + toastr[type](message); + } else if (typeof Swal !== "undefined") { + Swal.fire({ + title: "Peringatan Session", + text: message, + icon: type, + confirmButtonText: "OK", + allowOutsideClick: false, + timer: 5000, + timerProgressBar: true, + }); + } else { + // Create custom notification + this.createCustomNotification(message, type); + } + } + + createCustomNotification(message, type) { + const notification = document.createElement("div"); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === "warning" ? "#ffc107" : "#007bff"}; + color: white; + padding: 15px 20px; + border-radius: 5px; + z-index: 9999; + max-width: 400px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + font-family: Arial, sans-serif; + font-size: 14px; + `; + notification.innerHTML = ` + Peringatan!
+ ${message} + `; + + document.body.appendChild(notification); + + // Remove after 8 seconds + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 8000); + } + + clearStoredData() { + // Clear localStorage + try { + localStorage.clear(); + } catch (e) { + console.warn("Could not clear localStorage:", e); + } + + // Clear sessionStorage + try { + sessionStorage.clear(); + } catch (e) { + console.warn("Could not clear sessionStorage:", e); + } + } + + forceLogout() { + console.log("Forcing logout..."); + + // Try to logout via API first + fetch("/logout", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": + document + .querySelector('meta[name="csrf-token"]') + ?.getAttribute("content") || "", + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "include", + }) + .then(() => { + window.location.href = "/login"; + }) + .catch(() => { + // Force redirect even if logout fails + window.location.href = "/login"; + }); + } + + // Method untuk manual reset + reset() { + this.consecutiveErrors = 0; + this.isLoggingOut = false; + this.lastCheckTime = 0; + } + + // Method untuk manual logout + logout() { + this.handleTokenInvalid("Manual logout requested"); + } +} + +// Initialize when DOM is ready +document.addEventListener("DOMContentLoaded", () => { + window.apiTokenWebValidator = new ApiTokenWebValidator(); +}); + +// Export for module usage +if (typeof module !== "undefined" && module.exports) { + module.exports = ApiTokenWebValidator; +} diff --git a/resources/js/utils/multi-user-session-handler.js b/resources/js/utils/multi-user-session-handler.js new file mode 100644 index 0000000..ac01754 --- /dev/null +++ b/resources/js/utils/multi-user-session-handler.js @@ -0,0 +1,264 @@ +/** + * Multi-User Session Handler + * Menangani kasus ketika multiple user login dan session conflict + */ +class MultiUserSessionHandler { + constructor() { + this.checkInterval = null; + this.lastCheckTime = 0; + this.isChecking = false; + this.init(); + } + + init() { + console.log("Multi-User Session Handler initialized"); + + // Check session setiap 10 detik + this.startPeriodicCheck(); + + // Check session ketika tab menjadi aktif + document.addEventListener("visibilitychange", () => { + if (!document.hidden) { + this.checkSession(); + } + }); + + // Check session ketika window focus + window.addEventListener("focus", () => { + this.checkSession(); + }); + + // Check session ketika user melakukan interaksi + document.addEventListener("click", () => { + this.checkSession(); + }); + + // Check session ketika ada AJAX request + this.interceptAjaxRequests(); + } + + startPeriodicCheck() { + this.checkInterval = setInterval(() => { + this.checkSession(); + }, 10000); // 10 detik + } + + stopPeriodicCheck() { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + } + + async checkSession() { + // Prevent multiple simultaneous checks + if (this.isChecking) { + return; + } + + // Prevent checking too frequently + const now = Date.now(); + if (now - this.lastCheckTime < 5000) { + // 5 detik minimum interval + return; + } + + this.isChecking = true; + this.lastCheckTime = now; + + try { + const response = await fetch("/api/check-session", { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + "Cache-Control": "no-cache", + }, + credentials: "include", + }); + + if (response.status === 401) { + console.log("Session invalid detected, logging out..."); + this.handleSessionInvalid(); + } else if (response.status === 200) { + const data = await response.json(); + if (!data.valid) { + console.log("Session validation failed, logging out..."); + this.handleSessionInvalid(); + } + } + } catch (error) { + console.error("Session check failed:", error); + } finally { + this.isChecking = false; + } + } + + interceptAjaxRequests() { + // Intercept fetch requests + const originalFetch = window.fetch; + window.fetch = async (...args) => { + try { + const response = await originalFetch(...args); + + // Check if response is 401 + if (response.status === 401) { + console.log( + "401 detected in fetch request, checking session..." + ); + this.checkSession(); + } + + return response; + } catch (error) { + console.error("Fetch request failed:", error); + throw error; + } + }; + + // Intercept XMLHttpRequest + const originalXHROpen = XMLHttpRequest.prototype.open; + const originalXHRSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function (...args) { + this._url = args[1]; + return originalXHROpen.apply(this, args); + }; + + XMLHttpRequest.prototype.send = function (...args) { + const xhr = this; + const originalOnReadyStateChange = xhr.onreadystatechange; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 401) { + console.log( + "401 detected in XHR request, checking session..." + ); + window.multiUserSessionHandler.checkSession(); + } + + if (originalOnReadyStateChange) { + originalOnReadyStateChange.apply(xhr, arguments); + } + }; + + return originalXHRSend.apply(this, args); + }; + } + + handleSessionInvalid() { + this.stopPeriodicCheck(); + + // Show notification + this.showNotification( + "Session Anda telah berakhir karena user lain login. Silakan login ulang.", + "warning" + ); + + // Clear any stored data + this.clearStoredData(); + + // Redirect to login after 2 seconds + setTimeout(() => { + window.location.href = "/login"; + }, 2000); + } + + showNotification(message, type = "info") { + // Try different notification libraries + if (typeof toastr !== "undefined") { + toastr[type](message); + } else if (typeof Swal !== "undefined") { + Swal.fire({ + title: "Peringatan Session", + text: message, + icon: type, + confirmButtonText: "OK", + allowOutsideClick: false, + }); + } else if (typeof alert !== "undefined") { + alert(message); + } else { + // Create custom notification + this.createCustomNotification(message, type); + } + } + + createCustomNotification(message, type) { + const notification = document.createElement("div"); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === "warning" ? "#ffc107" : "#007bff"}; + color: white; + padding: 15px 20px; + border-radius: 5px; + z-index: 9999; + max-width: 400px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + `; + notification.textContent = message; + + document.body.appendChild(notification); + + // Remove after 5 seconds + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 5000); + } + + clearStoredData() { + // Clear localStorage + try { + localStorage.clear(); + } catch (e) { + console.warn("Could not clear localStorage:", e); + } + + // Clear sessionStorage + try { + sessionStorage.clear(); + } catch (e) { + console.warn("Could not clear sessionStorage:", e); + } + } + + // Method untuk manual logout + logout() { + this.stopPeriodicCheck(); + + fetch("/logout", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": + document + .querySelector('meta[name="csrf-token"]') + ?.getAttribute("content") || "", + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "include", + }) + .then(() => { + this.clearStoredData(); + window.location.href = "/login"; + }) + .catch(() => { + this.clearStoredData(); + window.location.href = "/login"; + }); + } +} + +// Initialize when DOM is ready +document.addEventListener("DOMContentLoaded", () => { + window.multiUserSessionHandler = new MultiUserSessionHandler(); +}); + +// Export for module usage +if (typeof module !== "undefined" && module.exports) { + module.exports = MultiUserSessionHandler; +} diff --git a/resources/js/utils/session-manager.js b/resources/js/utils/session-manager.js new file mode 100644 index 0000000..65c8622 --- /dev/null +++ b/resources/js/utils/session-manager.js @@ -0,0 +1,126 @@ +/** + * Session Manager untuk menangani multi-user session + */ +class SessionManager { + constructor() { + this.checkInterval = null; + this.init(); + } + + init() { + // Check session setiap 30 detik + this.startSessionCheck(); + + // Listen untuk visibility change (tab focus/blur) + document.addEventListener("visibilitychange", () => { + if (!document.hidden) { + this.checkSession(); + } + }); + + // Listen untuk storage events (multi-tab) + window.addEventListener("storage", (e) => { + if (e.key === "session_invalid") { + this.handleSessionInvalid(); + } + }); + } + + startSessionCheck() { + this.checkInterval = setInterval(() => { + this.checkSession(); + }, 30000); // 30 detik + } + + stopSessionCheck() { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + } + + async checkSession() { + try { + const response = await fetch("/api/check-session", { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "include", + }); + + if (response.status === 401) { + this.handleSessionInvalid(); + } + } catch (error) { + console.error("Session check failed:", error); + } + } + + handleSessionInvalid() { + this.stopSessionCheck(); + + // Show notification + this.showNotification( + "Session Anda telah berakhir. Silakan login ulang.", + "warning" + ); + + // Redirect to login after 3 seconds + setTimeout(() => { + window.location.href = "/login"; + }, 3000); + } + + showNotification(message, type = "info") { + // Check if notification library exists (like Toastr, SweetAlert, etc.) + if (typeof toastr !== "undefined") { + toastr[type](message); + } else if (typeof Swal !== "undefined") { + Swal.fire({ + title: "Peringatan", + text: message, + icon: type, + confirmButtonText: "OK", + }); + } else { + // Fallback to alert + alert(message); + } + } + + // Method untuk logout manual + logout() { + this.stopSessionCheck(); + + fetch("/logout", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-TOKEN": + document + .querySelector('meta[name="csrf-token"]') + ?.getAttribute("content") || "", + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "include", + }) + .then(() => { + window.location.href = "/login"; + }) + .catch(() => { + window.location.href = "/login"; + }); + } +} + +// Initialize session manager when DOM is ready +document.addEventListener("DOMContentLoaded", () => { + window.sessionManager = new SessionManager(); +}); + +// Export for module usage +if (typeof module !== "undefined" && module.exports) { + module.exports = SessionManager; +} diff --git a/resources/views/layouts/partials/vendor-scripts.blade.php b/resources/views/layouts/partials/vendor-scripts.blade.php index b239814..f00c064 100644 --- a/resources/views/layouts/partials/vendor-scripts.blade.php +++ b/resources/views/layouts/partials/vendor-scripts.blade.php @@ -6,6 +6,11 @@ @vite('resources/js/app.js') @endif + +@auth + +@endauth + @yield('scripts')