fix menu tax in data and fix session when multiple user login
This commit is contained in:
@@ -36,7 +36,9 @@ class UsersController extends Controller
|
|||||||
return UserResource::collection($query->paginate(config('app.paginate_per_page', 50)));
|
return UserResource::collection($query->paginate(config('app.paginate_per_page', 50)));
|
||||||
}
|
}
|
||||||
public function logout(Request $request){
|
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']);
|
return response()->json(['message' => 'logged out successfully']);
|
||||||
}
|
}
|
||||||
public function store(UsersRequest $request){
|
public function store(UsersRequest $request){
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ class AuthenticatedSessionController extends Controller
|
|||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Hapus token lama jika ada
|
// 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
|
// Buat token untuk API dengan scope dan expiration
|
||||||
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
||||||
@@ -48,6 +50,10 @@ class AuthenticatedSessionController extends Controller
|
|||||||
// Simpan token di session untuk digunakan di frontend
|
// Simpan token di session untuk digunakan di frontend
|
||||||
session(['api_token' => $token]);
|
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);
|
return redirect()->intended(RouteServiceProvider::HOME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +72,9 @@ class AuthenticatedSessionController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete existing tokens
|
// Delete existing tokens
|
||||||
$user->tokens()->delete();
|
\Laravel\Sanctum\PersonalAccessToken::where('tokenable_id', $user->id)
|
||||||
|
->where('tokenable_type', get_class($user))
|
||||||
|
->delete();
|
||||||
|
|
||||||
// Generate new token
|
// Generate new token
|
||||||
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
$tokenName = config('app.name', 'Laravel') . '-' . $user->id . '-' . time();
|
||||||
@@ -107,7 +115,9 @@ class AuthenticatedSessionController extends Controller
|
|||||||
public function destroy(Request $request)
|
public function destroy(Request $request)
|
||||||
{
|
{
|
||||||
if($request->user()){
|
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();
|
Auth::guard('web')->logout();
|
||||||
|
|||||||
167
app/Http/Middleware/ValidateApiTokenForWeb.php
Normal file
167
app/Http/Middleware/ValidateApiTokenForWeb.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
$middleware->alias([
|
||||||
|
'validate.api.token.web' => \App\Http\Middleware\ValidateApiTokenForWeb::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){
|
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $th){
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class MenuSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
|
Menu::whereIn('name', ['Data Pajak','Pajak'])->delete();
|
||||||
|
|
||||||
$menus = [
|
$menus = [
|
||||||
[
|
[
|
||||||
"name" => "Neng Bedas",
|
"name" => "Neng Bedas",
|
||||||
@@ -193,6 +195,12 @@ class MenuSeeder extends Seeder
|
|||||||
"icon" => null,
|
"icon" => null,
|
||||||
"sort_order" => 9,
|
"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) {
|
foreach ($menus as $menuData) {
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ class UsersRoleMenuSeeder extends Seeder
|
|||||||
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
|
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
|
||||||
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
||||||
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA',
|
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'PETA',
|
||||||
'Lap Pimpinan', 'Dalam Sistem', 'Luar Sistem', 'Google Sheets', 'TPA TPT',
|
'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',
|
'Approval Pejabat', 'Undangan', 'Rekap Pembayaran', 'Lap Rekap Data Pembayaran', 'Lap PBG (PTSP)', 'Lap Pertumbuhan'
|
||||||
'Pajak', 'Data Pajak'
|
|
||||||
])->get()->keyBy('name');
|
])->get()->keyBy('name');
|
||||||
|
|
||||||
// Define access levels for each role
|
// Define access levels for each role
|
||||||
@@ -37,7 +36,7 @@ class UsersRoleMenuSeeder extends Seeder
|
|||||||
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
'Menu', 'Role', 'Setting Dashboard', 'PBG', 'Reklame', 'Usaha atau Industri', 'Pariwisata',
|
||||||
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
|
'Lap Pariwisata', 'UMKM', 'Dashboard Potensi', 'Tata Ruang', 'PDAM', 'Dalam Sistem',
|
||||||
'Luar Sistem', 'Lap Pimpinan', 'Google Sheets', 'TPA TPT', 'Approval Pejabat',
|
'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',
|
'user' => ['Dashboard', 'Data', 'Laporan', 'Neng Bedas',
|
||||||
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
|
'Approval', 'Tools', 'Dashboard Pimpinan', 'Dashboard PBG', 'Users', 'Syncronize',
|
||||||
@@ -68,10 +67,10 @@ class UsersRoleMenuSeeder extends Seeder
|
|||||||
|
|
||||||
// Attach User to role super admin
|
// Attach User to role super admin
|
||||||
$accountSuperadmin = User::where('email', 'superadmin@sibedas.com')->first();
|
$accountSuperadmin = User::where('email', 'superadmin@sibedas.com')->first();
|
||||||
|
$accountDevelopment = User::where('email', 'development@sibedas.com')->first();
|
||||||
$accountUser = User::where('email', 'user@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]);
|
$accountSuperadmin->roles()->sync([$roles['superadmin']->id]);
|
||||||
|
$accountDevelopment->roles()->sync([$roles['superadmin']->id]);
|
||||||
$accountUser->roles()->sync([$roles['user']->id]);
|
$accountUser->roles()->sync([$roles['user']->id]);
|
||||||
// $accountDefault->roles()->sync([$roles['user']->id]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
233
public/js/utils/simple-session-validator.js
Normal file
233
public/js/utils/simple-session-validator.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -171,8 +171,9 @@ class ApiTokenManager {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url, mergedOptions);
|
const response = await fetch(url, mergedOptions);
|
||||||
|
|
||||||
// If unauthorized, try to generate new token
|
// If unauthorized, check if it's a session issue
|
||||||
if (response.status === 401 && this.hasToken()) {
|
if (response.status === 401) {
|
||||||
|
if (this.hasToken()) {
|
||||||
console.log('Token expired, generating new token...');
|
console.log('Token expired, generating new token...');
|
||||||
const newToken = await this.generateToken();
|
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;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API request failed:', error);
|
console.error('API request failed:', error);
|
||||||
throw 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
|
// Export singleton instance
|
||||||
|
|||||||
337
resources/js/utils/api-token-web-validator.js
Normal file
337
resources/js/utils/api-token-web-validator.js
Normal 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;
|
||||||
|
}
|
||||||
264
resources/js/utils/multi-user-session-handler.js
Normal file
264
resources/js/utils/multi-user-session-handler.js
Normal 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;
|
||||||
|
}
|
||||||
126
resources/js/utils/session-manager.js
Normal file
126
resources/js/utils/session-manager.js
Normal 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;
|
||||||
|
}
|
||||||
@@ -6,6 +6,11 @@
|
|||||||
@vite('resources/js/app.js')
|
@vite('resources/js/app.js')
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<!-- Simple Session Validator -->
|
||||||
|
@auth
|
||||||
|
<script src="{{ asset('js/utils/simple-session-validator.js') }}"></script>
|
||||||
|
@endauth
|
||||||
|
|
||||||
@yield('scripts')
|
@yield('scripts')
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -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');
|
Route::get('/quick-search/{uuid}/task-assignments', [QuickSearchController::class, 'task_assignments'])->name('api.quick-search-task-assignments');
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
Route::group(['middleware' => 'auth'], function(){
|
Route::group(['middleware' => ['auth', 'validate.api.token.web']], function(){
|
||||||
|
|
||||||
Route::get('', [BigDataController::class, 'index'])->name('any');
|
Route::get('', [BigDataController::class, 'index'])->name('any');
|
||||||
Route::get('/home', [HomeController::class, 'index'])->name('home');
|
Route::get('/home', [HomeController::class, 'index'])->name('home');
|
||||||
|
|||||||
Reference in New Issue
Block a user