Redirect to login page when no activity for 30 minutes. Show only facility AET and VM IP. Add default users for now because user management is not yet implemented.

This commit is contained in:
shankar 2024-12-11 18:10:17 +05:30
parent 262307f17d
commit 7c288da183
12 changed files with 159 additions and 38 deletions

1
.env
View File

@ -1,6 +1,7 @@
VITE_API_URL=http://localhost:3000/api/v1 VITE_API_URL=http://localhost:3000/api/v1
NODE_ENV=development NODE_ENV=development
#NODE_ENV=production
#Database Configuration #Database Configuration
DB_NAME=ve_router_db DB_NAME=ve_router_db

View File

@ -32,3 +32,6 @@ BEGIN
END // END //
DELIMITER ; DELIMITER ;
-- Automatically call the procedure after creation
CALL seed_common_router_data();

View File

@ -1,2 +1,3 @@
VITE_API_URL=http://${SERVER_IP:-localhost}:3000/api/v1 VITE_API_URL=http://${SERVER_IP:-localhost}:3000/api/v1
VITE_NODE_ENV=development VITE_NODE_ENV=development
#VITE_NODE_ENV=production

View File

@ -5,31 +5,38 @@ import { useNavigate } from 'react-router-dom';
const Login: React.FC = () => { const Login: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { setIsAuthenticated } = useAuth(); // Access the setIsAuthenticated function const { setIsAuthenticated } = useAuth();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [usernameError, setUsernameError] = useState(''); const [usernameError, setUsernameError] = useState('');
const [passwordError, setPasswordError] = useState(''); const [passwordError, setPasswordError] = useState('');
const [generalError, setGeneralError] = useState(''); const [generalError, setGeneralError] = useState('');
const [loading, setLoading] = useState(true); // Start with loading true to show spinner initially
const [sessionExpiredMessage, setSessionExpiredMessage] = useState<string | null>(null); // State to store the session expired message const [sessionExpiredMessage, setSessionExpiredMessage] = useState<string | null>(null);
// Simulate initial loading state or check for session expiry
useEffect(() => { useEffect(() => {
// Check if the URL has the sessionExpired query parameter
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const sessionExpired = urlParams.get('sessionExpired'); const sessionExpired = urlParams.get('sessionExpired');
if (sessionExpired) { if (sessionExpired) {
setSessionExpiredMessage('Session expired or something went wrong. Please log in again.'); setSessionExpiredMessage('Session expired or something went wrong. Please log in again.');
} }
}, []); // Empty dependency array means this will run only on component mount
// Simulate a delay to show loading spinner for a brief moment (or wait for other async checks)
setTimeout(() => {
setLoading(false); // Set loading to false after a certain period
}, 1000); // You can adjust the delay (in ms) to suit your needs
}, []);
const handleLogin = async (event: React.FormEvent) => { const handleLogin = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
setLoading(true); // Set loading to true when login starts
let hasError = false; let hasError = false;
// Validate username and password
if (!username.trim()) { if (!username.trim()) {
setUsernameError('Username is required'); setUsernameError('Username is required');
hasError = true; hasError = true;
@ -44,32 +51,40 @@ const Login: React.FC = () => {
setPasswordError(''); setPasswordError('');
} }
// Stop execution if there are validation errors if (hasError) {
if (hasError) return; setLoading(false); // If there are errors, stop the loading state
return;
}
try { try {
const result = await apiService.login(username, password); const result = await apiService.login(username, password);
if (result) { if (result) {
// Save tokens and user details in localStorage
localStorage.setItem('accessToken', result.accessToken); localStorage.setItem('accessToken', result.accessToken);
localStorage.setItem('refreshToken', result.refreshToken); localStorage.setItem('refreshToken', result.refreshToken);
localStorage.setItem('user', JSON.stringify(result.user)); localStorage.setItem('user', JSON.stringify(result.user));
localStorage.setItem('isLoggedIn', 'true'); localStorage.setItem('isLoggedIn', 'true');
// Update the authentication state in the context
setIsAuthenticated(true); setIsAuthenticated(true);
navigate('/', { replace: true })
// Redirect to Dashboard
//navigate('/');
window.location.href = '/';
} else { } else {
setGeneralError('Invalid username or password'); setGeneralError('Invalid username or password');
} }
} catch (error: any) { } catch (error: any) {
setGeneralError(error.message || 'An unexpected error occurred'); setGeneralError(error.message || 'An unexpected error occurred');
} finally {
setLoading(false); // Stop loading once the login attempt is finished
} }
}; };
// Render loading spinner initially
if (loading) {
return (
<div className="p-6 flex items-center justify-center mt-40"> {/* Adjusted margin-top */}
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
);
}
return ( return (
<div className="min-h-screen flex flex-col bg-gray-100"> <div className="min-h-screen flex flex-col bg-gray-100">
{/* Login Content */} {/* Login Content */}

View File

@ -8,6 +8,7 @@ import { RouterData } from '../../types';
import { apiService } from '../../services/api.service'; import { apiService } from '../../services/api.service';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; // Import the useAuth hook import { useAuth } from '../../contexts/AuthContext'; // Import the useAuth hook
import useIdleTimeout from '../../utils/useIdleTimeout';
interface User { interface User {
name: string; name: string;
@ -107,12 +108,17 @@ const DashboardLayout: React.FC = () => {
// Redirect to login page // Redirect to login page
//navigate('/login'); //navigate('/login');
window.location.href = '/login'; //window.location.href = '/login';
navigate('/login', { replace: true });
} catch (error) { } catch (error) {
console.error('Error during logout:', error); console.error('Error during logout:', error);
} }
}; };
// Use the idle timeout hook
useIdleTimeout(handleLogout);
const getSummary = (routerData: RouterData[]) => { const getSummary = (routerData: RouterData[]) => {
return { return {
total: routerData.length, total: routerData.length,
@ -129,7 +135,6 @@ const DashboardLayout: React.FC = () => {
}; };
const renderContent = () => { const renderContent = () => {
if (loading) { if (loading) {
return ( return (
@ -269,6 +274,7 @@ const DashboardLayout: React.FC = () => {
</div> </div>
</div> </div>
); );
}; };
export default DashboardLayout; export default DashboardLayout;

View File

@ -58,8 +58,7 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
<div className="bg-white p-3 rounded shadow-sm"> <div className="bg-white p-3 rounded shadow-sm">
<div className="grid gap-2"> <div className="grid gap-2">
<div><span className="font-medium">Facility AET:</span> {router.facilityAET}</div> <div><span className="font-medium">Facility AET:</span> {router.facilityAET}</div>
<div><span className="font-medium">OpenVPN IP:</span> {router.openvpnIp}</div> <div><span className="font-medium">Router VM IP:</span> {router.routerVmPrimaryIp}</div>
<div><span className="font-medium">Router VM Primary IP:</span> {router.routerVmPrimaryIp}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,13 @@
// router-dashboard/src/services/api.service.ts // router-dashboard/src/services/api.service.ts
import { RouterData, FilterType, BackendRouter } from '../types'; import { RouterData, FilterType, BackendRouter } from '../types';
const API_BASE_URL = 'http://localhost:3000/api/v1'; const API_BASE_URL = process.env.NODE_ENV === 'development'
? 'http://localhost:3000/api/v1'
: '/api/v1';
console.log("process.env.NODE_ENV", process.env.NODE_ENV);
console.log("API_BASE_URL", API_BASE_URL);
// Default request options for all API calls // Default request options for all API calls
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {

View File

@ -0,0 +1,62 @@
import { useEffect, useRef } from 'react';
/**
* Custom hook to manage user inactivity and trigger a timeout action.
* @param onTimeout - Function to execute when the timeout is reached.
* @param timeout - Duration (in ms) before timeout. Default: 30 minutes.
*/
const useIdleTimeout = (onTimeout: () => void, timeout: number = 30 * 60 * 1000) => {
// Ref to persist the timeout timer and prevent resets during re-renders
const timeoutTimerRef = useRef<NodeJS.Timeout | null>(null);
// Function to reset the timer
const resetTimer = () => {
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current);
}
timeoutTimerRef.current = setTimeout(onTimeout, timeout);
};
useEffect(() => {
// Event listener for user activity (mousemove, keydown, click)
const handleUserActivity = () => {
resetTimer();
};
// Event listener for tab visibility (page hidden or visible)
const handleVisibilityChange = () => {
if (document.hidden) {
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current); // Pause the timer if the tab is not visible
}
} else {
resetTimer(); // Restart the timer when the tab becomes visible again
}
};
// Add event listeners for user activity
window.addEventListener('mousemove', handleUserActivity);
window.addEventListener('keydown', handleUserActivity);
window.addEventListener('click', handleUserActivity);
// Add event listener for page visibility change
document.addEventListener('visibilitychange', handleVisibilityChange);
// Initialize the timer when the component mounts
resetTimer();
// Cleanup function to remove listeners and clear the timer
return () => {
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current); // Cleanup the timeout
}
window.removeEventListener('mousemove', handleUserActivity);
window.removeEventListener('keydown', handleUserActivity);
window.removeEventListener('click', handleUserActivity);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [onTimeout, timeout]); // Dependency array to only re-run on changes to onTimeout or timeout
};
export default useIdleTimeout;

View File

@ -19,7 +19,7 @@ app.use((req, res, next) => {
// CORS configuration // CORS configuration
app.use(cors({ app.use(cors({
origin: ['http://localhost:5173', 'http://localhost:3000'], origin: ['http://localhost:5173', 'https://router-dashboard.dev.vitalengine.com:3000', 'https://router-dashboard.vitalengine.com:3000'],
credentials: true, credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'] allowedHeaders: ['Content-Type', 'Authorization']

View File

@ -162,18 +162,45 @@ export class RouterRepository {
return vpnStatus === 'VPN_CONNECTED' && appStatus === 'CONTAINER_RUNNING' && vmStatus === 'NET_ONLINE' ? 'CONNECTED' : 'DISCONNECTED'; return vpnStatus === 'VPN_CONNECTED' && appStatus === 'CONTAINER_RUNNING' && vmStatus === 'NET_ONLINE' ? 'CONNECTED' : 'DISCONNECTED';
}; };
async updateRouterAndContainerStatus(id: number, vpnStatus: string, appStatus: string): Promise<void> {
// Update router status
await pool.query(
`UPDATE routers SET vpn_status_code = ?, app_status_code = ?, updated_at = NOW() WHERE id = ?`,
[vpnStatus, appStatus, id]
);
// Update container status
await pool.query(
`UPDATE container_status SET status_code = ?, updated_at = NOW() WHERE router_id = ?`,
["CONTAINER_STOPPED", id]
);
}
private async transformDatabaseRouter(dbRouter: any, index: number): Promise<RouterData> { private async transformDatabaseRouter(dbRouter: any, index: number): Promise<RouterData> {
try { try {
const studies = await this.getRouterStudies(dbRouter.id);
//const vms = await this.getRouterVMs(dbRouter.id);
const vms:VM[] = [];
const containers = await this.getRouterContainers(dbRouter.id);
const lastSeen = new Date(dbRouter.last_seen).toISOString(); const lastSeen = new Date(dbRouter.last_seen).toISOString();
const vpnStatus = dbRouter.vpn_status_code.toString(); const vpnStatus = dbRouter.vpn_status_code.toString();
const appStatus = dbRouter.app_status_code.toString(); const appStatus = dbRouter.app_status_code.toString();
const vmStatus = await this.calculateVMStatus(lastSeen); const vmStatus = await this.calculateVMStatus(lastSeen);
const routerStatus = await this.calculateSystemStatus(vpnStatus, appStatus, vmStatus); const routerStatus = await this.calculateSystemStatus(vpnStatus, appStatus, vmStatus);
// Update vpnStatus and appStatus based on vmStatus
let updatedVpnStatus = vpnStatus;
let updatedAppStatus = appStatus;
if (vmStatus === 'NET_OFFLINE') {
updatedVpnStatus = 'VPN_DISCONNECTED';
updatedAppStatus = 'CONTAINER_STOPPED';
await this.updateRouterAndContainerStatus(dbRouter.id, updatedVpnStatus, updatedAppStatus);
}
const studies = await this.getRouterStudies(dbRouter.id);
//const vms = await this.getRouterVMs(dbRouter.id);
const vms:VM[] = [];
const containers = await this.getRouterContainers(dbRouter.id);
return { return {
id: dbRouter.id, id: dbRouter.id,
slNo: index + 1, slNo: index + 1,
@ -192,8 +219,8 @@ export class RouterRepository {
studies studies
}, },
systemStatus: { systemStatus: {
vpnStatus: vpnStatus, vpnStatus: updatedVpnStatus,
appStatus: appStatus, appStatus: updatedAppStatus,
vmStatus: vmStatus, vmStatus: vmStatus,
routerStatus: routerStatus, routerStatus: routerStatus,
vms, vms,

View File

@ -19,8 +19,8 @@ export class AuthService {
return jwt.sign( return jwt.sign(
{ userId: user.id, username: user.username, role: user.role }, { userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET as string, process.env.JWT_SECRET as string,
//{ expiresIn: '30m' } { expiresIn: '30m' }
{ expiresIn: '1m' } //{ expiresIn: '1m' }
); );
}; };
@ -29,8 +29,8 @@ export class AuthService {
const accessToken = jwt.sign( const accessToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role }, { userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET as string, process.env.JWT_SECRET as string,
//{ expiresIn: '30m' } { expiresIn: '30m' }
{ expiresIn: '1m' } //{ expiresIn: '1m' }
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(

View File

@ -18,7 +18,8 @@ export class SetupService {
{ name: 'Administrator', username: 'admin', email: 'admin@ve.com', password: 'admin@@007', role: 'admin' }, { name: 'Administrator', username: 'admin', email: 'admin@ve.com', password: 'admin@@007', role: 'admin' },
{ name: 'Maqbool Patel', username: 'maqbool', email: 'maqbool@ve.com', password: 'maqbool@@210', role: 'admin' }, { name: 'Maqbool Patel', username: 'maqbool', email: 'maqbool@ve.com', password: 'maqbool@@210', role: 'admin' },
{ name: 'Kavya Raghunath', username: 'kavya', email: 'kavya@ve.com', password: 'kavya@@124', role: 'viewer' }, { name: 'Kavya Raghunath', username: 'kavya', email: 'kavya@ve.com', password: 'kavya@@124', role: 'viewer' },
{ name: 'Reid McKenzie', username: 'reid', email: 'reid@ve.com', password: 'reid@@321', role: 'viewer' } { name: 'Reid McKenzie', username: 'reid', email: 'reid@ve.com', password: 'reid@@321', role: 'viewer' },
{ name: 'Guest', username: 'guest', email: 'guest@ve.com', password: 'guest@@012', role: 'viewer' }
]; ];
const createdUsers = []; const createdUsers = [];