feat(auth): Implemented Session Persistence

This commit is contained in:
dhinesh-m24
2026-01-29 12:39:45 +05:30
parent e214e32c17
commit 7133e59e57
10 changed files with 417 additions and 32 deletions

View File

@@ -0,0 +1,127 @@
import { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import {
subscribeToAuthState,
refreshUserToken,
stopTokenRefreshTimer
} from '../services/authService';
import { checkAuthStatus, logoutUser } from '../features/auth/authSlice';
import type { RootState, AppDispatch } from '../store/store';
/**
* Custom hook for managing session persistence and token refresh
*
* Responsibilities:
* 1. Initialize auth state on app load and restore persisted sessions
* 2. Manage automatic token refresh to prevent session expiry
* 3. Detect and handle token expiration
* 4. Set up activity monitoring (optional, can extend for activity-based timeouts)
*
* Usage: Call this hook in AppLayout or a root component that wraps authenticated routes
*/
export const useSessionPersistence = () => {
const dispatch = useDispatch<AppDispatch>();
const navigate = useNavigate();
const { isAuthenticated, user } = useSelector((state: RootState) => state.auth);
/**
* Handle token expiration by logging out user and redirecting to login
*/
const handleTokenExpiration = useCallback(async () => {
console.warn('Token expired, logging out user');
await dispatch(logoutUser());
navigate('/login', { replace: true, state: { message: 'Your session has expired. Please log in again.' } });
}, [dispatch, navigate]);
/**
* Initialize session on component mount
* Restores persisted session from Firebase and sets up auth listeners
*/
useEffect(() => {
let unsubscribe: (() => void) | null = null;
const initializeSession = async () => {
try {
// Check if user is already logged in (from Firebase persistence)
await dispatch(checkAuthStatus());
// Set up real-time auth state listener
unsubscribe = subscribeToAuthState(async (firebaseUser) => {
if (firebaseUser) {
// User is authenticated - token refresh is started by subscribeToAuthState
console.log('User session restored:', firebaseUser.email);
} else {
// User is not authenticated
stopTokenRefreshTimer();
}
});
} catch (error) {
console.error('Error initializing session:', error);
}
};
initializeSession();
// Cleanup on unmount
return () => {
if (unsubscribe) {
unsubscribe();
}
stopTokenRefreshTimer();
};
}, [dispatch]);
/**
* Monitor token validity and handle expiration
* Periodically checks if token is still valid
*/
useEffect(() => {
if (!isAuthenticated || !user) return;
// Set up interval to check token validity every 5 minutes
const tokenCheckInterval = window.setInterval(async () => {
try {
// Attempt to get fresh token - this will throw if token is invalid/expired
const success = await refreshUserToken();
if (!success) {
// Token refresh failed
handleTokenExpiration();
}
} catch (error) {
console.error('Token validation failed:', error);
handleTokenExpiration();
}
}, 5 * 60 * 1000); // Check every 5 minutes
// Cleanup interval
return () => clearInterval(tokenCheckInterval);
}, [isAuthenticated, user, handleTokenExpiration]);
/**
* Update last activity timestamp on user interaction
* This can be used to implement idle timeout if needed in the future
*/
useEffect(() => {
if (!isAuthenticated) return;
const updateActivity = () => {
localStorage.setItem('lastActivityTime', Date.now().toString());
};
// Track user activity
const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click'];
events.forEach(event => {
window.addEventListener(event, updateActivity);
});
// Cleanup event listeners
return () => {
events.forEach(event => {
window.removeEventListener(event, updateActivity);
});
};
}, [isAuthenticated]);
};