feat(auth): Implemented Session Persistence
This commit is contained in:
127
apps/web/src/hooks/useSessionPersistence.ts
Normal file
127
apps/web/src/hooks/useSessionPersistence.ts
Normal 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]);
|
||||
};
|
||||
Reference in New Issue
Block a user