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