feat: implement AuthPageGuard and GlobalCredentialChecker to enforce session management and token validation

This commit is contained in:
Firman Ramdhani
2026-08-03 12:42:09 +07:00
parent 9e30c8a5e5
commit 26c5a6faaa
3 changed files with 113 additions and 15 deletions
+8 -5
View File
@@ -1,14 +1,17 @@
import { lazy } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { AuthPageGuard } from '../../core/lib/auth-guard';
const LoginPage = lazy(() => import('./login'));
export default function AuthModule() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<Navigate to="/auth/login" replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes>
<AuthPageGuard>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<Navigate to="/auth/login" replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes>
</AuthPageGuard>
);
}
+13 -10
View File
@@ -1,6 +1,7 @@
import { lazy } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import ModuleLayout from './layouts/module.layout';
import { GlobalCredentialChecker } from '../../core/lib/auth-guard';
const ExampleModule = lazy(() => import('./modules/example'));
const SystemSetting = lazy(() => import('./modules/system/setting'));
@@ -9,15 +10,17 @@ const SystemNotification = lazy(() => import('./modules/system/notification'));
export default function AppModule() {
return (
<ModuleLayout>
<Routes>
<Route path="/example/*" element={<ExampleModule />} />
<Route path="/system/setting" element={<SystemSetting />} />
<Route path="/system/information" element={<SystemInformation />} />
<Route path="/system/notifications" element={<SystemNotification />} />
<Route path="/" element={<Navigate to="/app/example/full-page" replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes>
</ModuleLayout>
<GlobalCredentialChecker>
<ModuleLayout>
<Routes>
<Route path="/example/*" element={<ExampleModule />} />
<Route path="/system/setting" element={<SystemSetting />} />
<Route path="/system/information" element={<SystemInformation />} />
<Route path="/system/notifications" element={<SystemNotification />} />
<Route path="/" element={<Navigate to="/app/example/full-page" replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes>
</ModuleLayout>
</GlobalCredentialChecker>
);
}
+92
View File
@@ -0,0 +1,92 @@
import { useEffect, useState, ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { appDatabase, AppDatabaseKey } from '../storage/local';
import { terminateAuthSession } from './auth.helper';
/**
* Basic JWT decoder to check expiration.
*/
function isTokenExpired(token: string): boolean {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.exp) {
return payload.exp * 1000 < Date.now();
}
return false; // If no exp, assume valid
} catch (e) {
return true; // Invalid token format -> treat as expired
}
}
/**
* For Auth Page (Login/Register).
* - If valid token exists -> Redirect to main application.
* - If expired token exists -> Clear session, stay on auth page.
*/
export function AuthPageGuard({ children }: { children: ReactNode }) {
const navigate = useNavigate();
const [isChecking, setIsChecking] = useState(true);
useEffect(() => {
async function checkCredential() {
try {
const token = await appDatabase.getItem<string>(AppDatabaseKey.ACCESS_TOKEN);
if (token) {
if (!isTokenExpired(token)) {
// Valid token exists, redirect to dashboard/main app
navigate('/app', { replace: true });
return;
} else {
// Token exists but is expired. Terminate session without redirecting (we are already in Auth).
await terminateAuthSession({ preserveRedirect: false });
}
}
setIsChecking(false);
} catch (error) {
console.error('Failed to check credentials in AuthPageGuard:', error);
setIsChecking(false);
}
}
checkCredential();
}, [navigate]);
if (isChecking) return null;
return <>{children}</>;
}
/**
* For Root/Main App.
* - Monitors token status.
* - If expired -> Immediately terminate session and redirect to login.
*/
export function GlobalCredentialChecker({ children }: { children: ReactNode }) {
useEffect(() => {
let intervalId: any;
async function checkCredential() {
try {
const token = await appDatabase.getItem<string>(AppDatabaseKey.ACCESS_TOKEN);
if (!token || isTokenExpired(token)) {
// Token is missing or expired -> clear session and redirect to login
await terminateAuthSession({ preserveRedirect: true });
}
} catch (error) {
console.error('Failed to check credentials in GlobalCredentialChecker:', error);
await terminateAuthSession({ preserveRedirect: true });
}
}
// Immediate check on mount
checkCredential();
// Periodic check every 1 minute
// eslint-disable-next-line prefer-const
intervalId = setInterval(checkCredential, 60000);
return () => clearInterval(intervalId);
}, []);
return <>{children}</>;
}