fix menu tax in data and fix session when multiple user login

This commit is contained in:
arifal hidayat
2025-08-07 00:51:46 +07:00
parent 0abf278aa3
commit af05a39a82
13 changed files with 1209 additions and 36 deletions

View File

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

View File

@@ -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();
@@ -48,6 +50,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();

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Laravel\Sanctum\PersonalAccessToken;
use Symfony\Component\HttpFoundation\Response;
class ValidateApiTokenForWeb
{
/**
* Handle an incoming request.
* Middleware ini memvalidasi token API untuk web requests
* dan melakukan auto-logout jika token tidak valid
*/
public function handle(Request $request, Closure $next): Response
{
// Skip validation untuk non-authenticated routes
if (!Auth::check()) {
return $next($request);
}
// Skip validation untuk API routes (sudah ditangani oleh auth:sanctum)
if ($request->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);
}
}

View File

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

View File

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

View File

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

View File

@@ -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 = `
<strong>Peringatan!</strong><br>
${message}<br>
<small>Anda akan diarahkan ke halaman login...</small>
`;
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;
}

View File

@@ -171,8 +171,9 @@ class ApiTokenManager {
try {
const response = await fetch(url, mergedOptions);
// If unauthorized, try to generate new token
if (response.status === 401 && this.hasToken()) {
// 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();
@@ -183,12 +184,45 @@ class ApiTokenManager {
}
}
// If still 401, it might be a session issue
console.log('Session invalid, redirecting to login...');
this.handleSessionInvalid();
return response;
}
return response;
} catch (error) {
console.error('API request failed:', error);
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

View File

@@ -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 = `
<strong>Peringatan!</strong><br>
${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;
}

View File

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

View File

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

View File

@@ -6,6 +6,11 @@
@vite('resources/js/app.js')
@endif
<!-- Simple Session Validator -->
@auth
<script src="{{ asset('js/utils/simple-session-validator.js') }}"></script>
@endauth
@yield('scripts')
<script>

View File

@@ -44,7 +44,7 @@ Route::get('/quick-search/{id}', [QuickSearchController::class, 'show'])->name('
Route::get('/quick-search/{uuid}/task-assignments', [QuickSearchController::class, 'task_assignments'])->name('api.quick-search-task-assignments');
// auth
Route::group(['middleware' => 'auth'], function(){
Route::group(['middleware' => ['auth', 'validate.api.token.web']], function(){
Route::get('', [BigDataController::class, 'index'])->name('any');
Route::get('/home', [HomeController::class, 'index'])->name('home');