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
NODE_ENV=development
#NODE_ENV=production
#Database Configuration
DB_NAME=ve_router_db

View File

@ -32,3 +32,6 @@ BEGIN
END //
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_NODE_ENV=development
VITE_NODE_ENV=development
#VITE_NODE_ENV=production

View File

@ -5,71 +5,86 @@ import { useNavigate } from 'react-router-dom';
const Login: React.FC = () => {
const navigate = useNavigate();
const { setIsAuthenticated } = useAuth(); // Access the setIsAuthenticated function
const { setIsAuthenticated } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [usernameError, setUsernameError] = useState('');
const [passwordError, setPasswordError] = 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(() => {
// Check if the URL has the sessionExpired query parameter
const urlParams = new URLSearchParams(window.location.search);
const sessionExpired = urlParams.get('sessionExpired');
if (sessionExpired) {
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) => {
event.preventDefault();
setLoading(true); // Set loading to true when login starts
let hasError = false;
// Validate username and password
if (!username.trim()) {
setUsernameError('Username is required');
hasError = true;
} else {
setUsernameError('');
}
if (!password.trim()) {
setPasswordError('Password is required');
hasError = true;
} else {
setPasswordError('');
}
// Stop execution if there are validation errors
if (hasError) return;
if (hasError) {
setLoading(false); // If there are errors, stop the loading state
return;
}
try {
const result = await apiService.login(username, password);
if (result) {
// Save tokens and user details in localStorage
localStorage.setItem('accessToken', result.accessToken);
localStorage.setItem('refreshToken', result.refreshToken);
localStorage.setItem('user', JSON.stringify(result.user));
localStorage.setItem('isLoggedIn', 'true');
// Update the authentication state in the context
setIsAuthenticated(true);
// Redirect to Dashboard
//navigate('/');
window.location.href = '/';
navigate('/', { replace: true })
} else {
setGeneralError('Invalid username or password');
}
} catch (error: any) {
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 (
<div className="min-h-screen flex flex-col bg-gray-100">
{/* Login Content */}

View File

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

View File

@ -1,7 +1,13 @@
// router-dashboard/src/services/api.service.ts
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
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
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,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
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';
};
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> {
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 vpnStatus = dbRouter.vpn_status_code.toString();
const appStatus = dbRouter.app_status_code.toString();
const vmStatus = await this.calculateVMStatus(lastSeen);
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 {
id: dbRouter.id,
slNo: index + 1,
@ -192,8 +219,8 @@ export class RouterRepository {
studies
},
systemStatus: {
vpnStatus: vpnStatus,
appStatus: appStatus,
vpnStatus: updatedVpnStatus,
appStatus: updatedAppStatus,
vmStatus: vmStatus,
routerStatus: routerStatus,
vms,

View File

@ -19,8 +19,8 @@ export class AuthService {
return jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET as string,
//{ expiresIn: '30m' }
{ expiresIn: '1m' }
{ expiresIn: '30m' }
//{ expiresIn: '1m' }
);
};
@ -29,8 +29,8 @@ export class AuthService {
const accessToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET as string,
//{ expiresIn: '30m' }
{ expiresIn: '1m' }
{ expiresIn: '30m' }
//{ expiresIn: '1m' }
);
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: '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: '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 = [];