From 26c5a6faaa18440b8bd342c21424c71dc6700f88 Mon Sep 17 00:00:00 2001
From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com>
Date: Mon, 3 Aug 2026 12:42:09 +0700
Subject: [PATCH] feat: implement AuthPageGuard and GlobalCredentialChecker to
enforce session management and token validation
---
apps/web/src/apps/auth/index.tsx | 13 ++--
apps/web/src/apps/main/index.tsx | 23 ++++---
apps/web/src/core/lib/auth-guard.tsx | 92 ++++++++++++++++++++++++++++
3 files changed, 113 insertions(+), 15 deletions(-)
create mode 100644 apps/web/src/core/lib/auth-guard.tsx
diff --git a/apps/web/src/apps/auth/index.tsx b/apps/web/src/apps/auth/index.tsx
index 4eb4320..63edda4 100644
--- a/apps/web/src/apps/auth/index.tsx
+++ b/apps/web/src/apps/auth/index.tsx
@@ -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 (
-
- } />
- } />
- } />
-
+
+
+ } />
+ } />
+ } />
+
+
);
}
diff --git a/apps/web/src/apps/main/index.tsx b/apps/web/src/apps/main/index.tsx
index 43a0893..dd55074 100644
--- a/apps/web/src/apps/main/index.tsx
+++ b/apps/web/src/apps/main/index.tsx
@@ -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 (
-
-
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
);
}
diff --git a/apps/web/src/core/lib/auth-guard.tsx b/apps/web/src/core/lib/auth-guard.tsx
new file mode 100644
index 0000000..7e4078e
--- /dev/null
+++ b/apps/web/src/core/lib/auth-guard.tsx
@@ -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(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(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}>;
+}