Added code to validate if user authenticated then redirect to dashboard else login page. Added code to handle role based navebar tab displaying. Added code to handle access token or refresh token expiration and then redirect to login page.

This commit is contained in:
shankar 2024-12-09 17:06:01 +05:30
parent ea7ac4cc72
commit 262307f17d
17 changed files with 432 additions and 130 deletions

View File

@ -106,7 +106,6 @@ CREATE TABLE IF NOT EXISTS users (
CREATE TABLE IF NOT EXISTS user_sessions ( CREATE TABLE IF NOT EXISTS user_sessions (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL, user_id INT NOT NULL,
session_token VARCHAR(255) UNIQUE NOT NULL,
refresh_token VARCHAR(255) NOT NULL, refresh_token VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL, ip_address VARCHAR(45) NOT NULL,
user_agent TEXT, user_agent TEXT,
@ -115,7 +114,6 @@ CREATE TABLE IF NOT EXISTS user_sessions (
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_user_session_user FOREIGN KEY (user_id) CONSTRAINT fk_user_session_user FOREIGN KEY (user_id)
REFERENCES users(id) ON DELETE CASCADE, REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT unique_session_token UNIQUE(session_token),
CONSTRAINT unique_refresh_token UNIQUE(refresh_token) CONSTRAINT unique_refresh_token UNIQUE(refresh_token)
); );

View File

@ -13,6 +13,7 @@
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.14.1",
"router-dashboard": "file:" "router-dashboard": "file:"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,20 +1,35 @@
import React, { useState, useEffect } from 'react'; import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import Login from './components/Login'; import Login from './components/Login';
import DashboardLayout from './components/dashboard/DashboardLayout'; import DashboardLayout from './components/dashboard/DashboardLayout';
import { useAuth } from './contexts/AuthContext';
function App() { function App() {
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false); const { isAuthenticated } = useAuth();
// Simulating checking if the user is logged in (e.g., using localStorage or an API call)
useEffect(() => {
const userLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; // Example logic
setIsLoggedIn(userLoggedIn);
}, []);
return ( return (
<> <Routes>
{isLoggedIn ? <DashboardLayout /> : <Login />} <Route
</> path="/"
element={
isAuthenticated ? (
<DashboardLayout />
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/login"
element={
isAuthenticated ? (
<Navigate to="/" replace />
) : (
<Login />
)
}
/>
</Routes>
); );
} }

View File

@ -1,33 +1,83 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import Header from './dashboard/Header'; import { useAuth } from '../contexts/AuthContext';
import { apiService } from '../services/api.service';
import { useNavigate } from 'react-router-dom';
const Login: React.FC = () => { const Login: React.FC = () => {
const navigate = useNavigate();
const { setIsAuthenticated } = useAuth(); // Access the setIsAuthenticated function
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [error, setError] = useState(''); const [usernameError, setUsernameError] = useState('');
const [passwordError, setPasswordError] = useState('');
const [generalError, setGeneralError] = useState('');
const handleLogin = (event: React.FormEvent) => { const [sessionExpiredMessage, setSessionExpiredMessage] = useState<string | null>(null); // State to store the session expired message
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
const handleLogin = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
// Mock authentication logic let hasError = false;
if (username === 'admin' && password === 'password') {
localStorage.setItem('user', JSON.stringify({ accessToken: 'fake-token' })); // Validate username and password
window.location.href = '/dashboard'; if (!username.trim()) {
setUsernameError('Username is required');
hasError = true;
} else { } else {
setError('Invalid username or password'); setUsernameError('');
}
if (!password.trim()) {
setPasswordError('Password is required');
hasError = true;
} else {
setPasswordError('');
}
// Stop execution if there are validation errors
if (hasError) 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 = '/';
} else {
setGeneralError('Invalid username or password');
}
} catch (error: any) {
setGeneralError(error.message || 'An unexpected error occurred');
} }
}; };
return ( return (
<div className="min-h-screen flex flex-col bg-gray-100"> <div className="min-h-screen flex flex-col bg-gray-100">
{/* Header */}
<Header user={{}} onLogout={() => {}} />
{/* Login Content */} {/* Login Content */}
<div className="flex flex-1 items-center justify-center"> <div className="flex flex-1 items-center justify-center">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow"> <div className="max-w-md w-full bg-white p-8 rounded-lg shadow">
<h2 className="text-2xl font-semibold text-center text-gray-700 mb-6">Login</h2> <h2 className="text-2xl font-semibold text-center text-gray-700 mb-6">Login</h2>
<form onSubmit={handleLogin}> <form onSubmit={handleLogin}>
{/* Username Field */}
<div className="mb-4"> <div className="mb-4">
<label htmlFor="username" className="block text-sm font-medium text-gray-700"> <label htmlFor="username" className="block text-sm font-medium text-gray-700">
Username Username
@ -40,7 +90,10 @@ const Login: React.FC = () => {
className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter your username" placeholder="Enter your username"
/> />
{usernameError && <div className="text-red-500 text-sm mt-1">{usernameError}</div>}
</div> </div>
{/* Password Field */}
<div className="mb-6"> <div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-700"> <label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password Password
@ -53,10 +106,16 @@ const Login: React.FC = () => {
className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500" className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter your password" placeholder="Enter your password"
/> />
{passwordError && <div className="text-red-500 text-sm mt-1">{passwordError}</div>}
</div> </div>
{error && (
<div className="text-red-500 text-sm mb-4">{error}</div> {/* General Error */}
)} {generalError && <div className="text-red-500 text-sm mb-4">{generalError}</div>}
{/* Display the session expired message */}
{sessionExpiredMessage && <div className="text-red-500 text-sm mb-4">{sessionExpiredMessage}</div>}
{/* Login Button */}
<button <button
type="submit" type="submit"
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-150" className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-150"

View File

@ -6,6 +6,8 @@ import RouterTable from './RouterTable';
import RouterManagement from './pages/RouterManagement'; import RouterManagement from './pages/RouterManagement';
import { RouterData } from '../../types'; import { RouterData } from '../../types';
import { apiService } from '../../services/api.service'; import { apiService } from '../../services/api.service';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; // Import the useAuth hook
interface User { interface User {
name: string; name: string;
@ -15,15 +17,36 @@ interface User {
type FilterType = 'all' | 'active' | 'critical' | 'diskAlert'; type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
const DashboardLayout: React.FC = () => { const DashboardLayout: React.FC = () => {
const navigate = useNavigate();
const { isAuthenticated, setIsAuthenticated } = useAuth(); // Use AuthContext
const [activeTab, setActiveTab] = useState('dashboard'); const [activeTab, setActiveTab] = useState('dashboard');
const [activeFilter, setActiveFilter] = useState<FilterType>('all'); const [activeFilter, setActiveFilter] = useState<FilterType>('all');
const [routers, setRouters] = useState<RouterData[]>([]); const [routers, setRouters] = useState<RouterData[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [user] = useState<User>({ const [user, setUser] = useState<User | null>(null);
name: 'John Doe',
role: 'Administrator' useEffect(() => {
}); const fetchUser = async () => {
if (isAuthenticated) {
try {
const storedUser = localStorage.getItem('user');
if (storedUser) {
const { name, role }: { name: string; role: string } = JSON.parse(storedUser);
setUser({ name, role });
}
} catch (error) {
console.error('Error parsing user from localStorage:', error);
setUser(null);
}
}
};
fetchUser(); // Call the async function
}, [isAuthenticated]);
useEffect(() => { useEffect(() => {
const fetchRouters = async () => { const fetchRouters = async () => {
@ -64,9 +87,30 @@ const DashboardLayout: React.FC = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [activeFilter]); }, [activeFilter]);
const handleLogout = () => { // Handle logout functionality
const handleLogout = async () => {
console.log('Logging out...'); console.log('Logging out...');
// Add your logout logic here try {
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
await apiService.logout(refreshToken); // Call logout API if available
}
// Remove only authentication-related items from localStorage
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
setUser(null); // Set user state to null after logging out
setIsAuthenticated(false); // Update the authentication state in context
// Redirect to login page
//navigate('/login');
window.location.href = '/login';
} catch (error) {
console.error('Error during logout:', error);
}
}; };
const getSummary = (routerData: RouterData[]) => { const getSummary = (routerData: RouterData[]) => {
@ -218,7 +262,7 @@ const DashboardLayout: React.FC = () => {
<div className="min-h-screen bg-gray-100"> <div className="min-h-screen bg-gray-100">
<Header user={user} onLogout={handleLogout} /> <Header user={user} onLogout={handleLogout} />
<div className="flex h-[calc(100vh-4rem)]"> <div className="flex h-[calc(100vh-4rem)]">
<Navbar activeTab={activeTab} onTabChange={setActiveTab} /> <Navbar activeTab={activeTab} onTabChange={setActiveTab} role={user?.role} />
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
{renderContent()} {renderContent()}
</main> </main>

View File

@ -9,11 +9,15 @@ interface User {
} }
interface HeaderProps { interface HeaderProps {
user: User; user: User | null; // Allow user to be null
onLogout: () => void; onLogout: () => void;
} }
const Header: React.FC<HeaderProps> = ({ user, onLogout }) => { const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
if (!user) {
return null; // Don't render the header if the user is not available
}
return ( return (
<header className="bg-white shadow-sm"> <header className="bg-white shadow-sm">
<div className="h-16 px-4 flex items-center justify-between"> <div className="h-16 px-4 flex items-center justify-between">

View File

@ -13,26 +13,29 @@ import {
interface NavbarProps { interface NavbarProps {
activeTab: string; activeTab: string;
onTabChange: (tab: string) => void; onTabChange: (tab: string) => void;
role: string;
} }
interface Tab { interface Tab {
id: string; id: string;
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
roles: string[]; // Added roles to specify allowed roles
} }
const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => { const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange, role }) => {
const [isCollapsed, setIsCollapsed] = useState(true); const [isCollapsed, setIsCollapsed] = useState(true);
const [isPinned, setIsPinned] = useState(false); const [isPinned, setIsPinned] = useState(false);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const TABS_WITH_PERMISSIONS: Tab[] = [
const tabs: Tab[] = [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard, roles: ['admin', 'operator', 'viewer'] },
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, { id: 'routers', label: 'Router Management', icon: RouterIcon, roles: ['admin', 'operator'] },
{ id: 'routers', label: 'Router Management', icon: RouterIcon }, { id: 'users', label: 'User Management', icon: Users, roles: ['admin'] },
{ id: 'users', label: 'User Management', icon: Users }, { id: 'settings', label: 'Settings', icon: Settings, roles: ['admin'] },
{ id: 'settings', label: 'Settings', icon: Settings }
]; ];
const filteredTabs = TABS_WITH_PERMISSIONS.filter((tab) => tab.roles.includes(role));
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (!isPinned) { if (!isPinned) {
setIsCollapsed(false); setIsCollapsed(false);
@ -71,7 +74,6 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
{/* Header with title and controls */}
<div className="p-4 border-b border-gray-700 flex items-center justify-between"> <div className="p-4 border-b border-gray-700 flex items-center justify-between">
{!isCollapsed && ( {!isCollapsed && (
<div> <div>
@ -79,7 +81,11 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
<h2 className="text-sm font-semibold">System</h2> <h2 className="text-sm font-semibold">System</h2>
</div> </div>
)} )}
<div className={`flex items-center gap-2 ${isCollapsed ? 'w-full justify-center' : 'justify-end'}`}> <div
className={`flex items-center gap-2 ${
isCollapsed ? 'w-full justify-center' : 'justify-end'
}`}
>
{(isHovered || !isCollapsed) && ( {(isHovered || !isCollapsed) && (
<button <button
onClick={togglePin} onClick={togglePin}
@ -90,7 +96,9 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
> >
<Pin <Pin
size={16} size={16}
className={`transform transition-transform ${isPinned ? 'rotate-45' : ''}`} className={`transform transition-transform ${
isPinned ? 'rotate-45' : ''
}`}
/> />
</button> </button>
)} )}
@ -99,18 +107,14 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
className="p-1 rounded hover:bg-gray-700 transition-colors" className="p-1 rounded hover:bg-gray-700 transition-colors"
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'} title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
> >
{isCollapsed ? ( {isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
<ChevronRight size={16} />
) : (
<ChevronLeft size={16} />
)}
</button> </button>
</div> </div>
</div> </div>
{/* Navigation Items */} {/* Navigation Items */}
<nav className="flex-1 p-2"> <nav className="flex-1 p-2">
{tabs.map(tab => { {filteredTabs.map((tab) => {
const Icon = tab.icon; const Icon = tab.icon;
return ( return (
<button <button
@ -119,9 +123,11 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
className={` className={`
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
transition-colors duration-200 group transition-colors duration-200 group
${activeTab === tab.id ${
activeTab === tab.id
? 'bg-blue-600 text-white' ? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-800'} : 'text-gray-300 hover:bg-gray-800'
}
`} `}
title={isCollapsed ? tab.label : undefined} title={isCollapsed ? tab.label : undefined}
> >
@ -131,41 +137,6 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
); );
})} })}
</nav> </nav>
{/* Expanded overlay when collapsed and hovered */}
{isHovered && isCollapsed && !isPinned && (
<div
className="absolute left-16 top-0 bg-gray-900 text-white h-full w-64 shadow-lg z-50 overflow-hidden"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="p-4 border-b border-gray-700">
<h2 className="text-sm font-semibold">Router Management</h2>
<h2 className="text-sm font-semibold">System</h2>
</div>
<nav className="p-2">
{tabs.map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
transition-colors duration-200
${activeTab === tab.id
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-800'}
`}
>
<Icon size={20} />
<span>{tab.label}</span>
</button>
);
})}
</nav>
</div>
)}
</div> </div>
); );
}; };

View File

@ -0,0 +1,41 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
interface AuthContextType {
isAuthenticated: boolean;
setIsAuthenticated: (value: boolean) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
export const AuthProvider: React.FC = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
// Check localStorage on initial load
return localStorage.getItem('isLoggedIn') === 'true';
});
useEffect(() => {
// Sync authentication state with localStorage
if (isAuthenticated) {
localStorage.setItem('isLoggedIn', 'true');
} else {
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
}
}, [isAuthenticated]);
return (
<AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
{children}
</AuthContext.Provider>
);
};

View File

@ -1,11 +1,17 @@
// File: src/main.tsx // File: src/main.tsx
import React from 'react' import React from 'react';
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client';
import App from './App.tsx' import { BrowserRouter as Router } from 'react-router-dom'; // Import BrowserRouter for routing
import './index.css' import App from './App.tsx'; // Ensure App is properly imported
import './index.css';
import { AuthProvider } from './contexts/AuthContext'; // Import the AuthProvider
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<AuthProvider> {/* Wrap the App with AuthProvider to enable authentication context */}
<Router> {/* Wrap the App with Router to enable routing */}
<App /> <App />
</React.StrictMode>, </Router>
) </AuthProvider>
</React.StrictMode>
);

View File

@ -11,6 +11,35 @@ const DEFAULT_OPTIONS = {
} }
}; };
// Helper function to get Authorization header
const getAuthHeaders = () => {
const accessToken = localStorage.getItem('accessToken');
if (!accessToken) {
console.error('No access token found in localStorage');
return {};
}
return {
Authorization: `Bearer ${accessToken}`,
};
};
// Helper function to remove tokens from localStorage
const removeTokens = () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
localStorage.removeItem('isLoggedIn');
};
const redirectToLogin = (logMessage: string) => {
console.log(logMessage);
removeTokens(); // Remove all tokens if refresh token expired
// Redirect to login page with a query parameter indicating session expiry
window.location.href = '/login?sessionExpired=true'; // Redirect with the query parameter
}
// Helper function to log API responses in development // Helper function to log API responses in development
const logResponse = (prefix: string, data: any) => { const logResponse = (prefix: string, data: any) => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@ -19,9 +48,131 @@ const logResponse = (prefix: string, data: any) => {
}; };
class ApiService { class ApiService {
async login(username: string, password: string): Promise<{ accessToken: string; refreshToken: string; user: any } | null> {
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error('Invalid username or password');
}
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
user: data.user,
};
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
// Refresh access token using the refresh token
async refreshToken(): Promise<boolean> {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
console.error('No refresh token found in localStorage');
return false;
}
try {
const response = await fetch(`${API_BASE_URL}/auth/refresh-token`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
console.log('Failed to refresh token: Expired or invalid token');
return false;
}
const data = await response.json();
// Save the new access and refresh tokens
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
return true;
} catch (error) {
console.error('Error refreshing token:', error);
return false;
}
}
// API call with token handling for expired access token or refresh token
async fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
// Get the Authorization headers dynamically
const authOptions = getAuthHeaders();
// Merge the passed options with the Authorization header inside the headers object
const finalOptions = {
...options, // Include user-specified options like method, body, etc.
headers: {
...(options.headers || {}), // Include existing headers from the passed options
...authOptions, // Add Authorization header dynamically
},
};
// Print the request details before sending it
//console.log('Request Details:');
//console.log('URL:', url);
//console.log('Options:', JSON.stringify(finalOptions, null, 2)); // Pretty print the options
const response = await fetch(url, { ...finalOptions });
if (response.status === 401) {
const errorData = await response.json();
// Check for Access token expired
if (errorData.message === 'Access token expired') {
console.log("Access token is expired, initiating to re generate")
const refreshed = await this.refreshToken();
if (refreshed) {
// Get the Authorization headers dynamically
const authOptions = getAuthHeaders();
// Retry the original request with new access token
return fetch(url, { ...options, ...authOptions });
} else {
redirectToLogin("Refresh token is expired, removing all stored session data");
}
}
// Check for Refresh token expired
if (errorData.message === 'Refresh token expired') {
redirectToLogin("Refresh token is expired, removing all stored session data");
}
redirectToLogin(errorData.message);
}
return response;
}
async logout(refreshToken: string): Promise<void> {
try {
const response = await this.fetchWithAuth(`${API_BASE_URL}/auth/logout`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Failed to logout');
}
await response.json();
} catch (error) {
console.error('Logout error:', error);
throw error;
}
}
async getAllRouters(filter: FilterType = 'all'): Promise<RouterData[]> { async getAllRouters(filter: FilterType = 'all'): Promise<RouterData[]> {
try { try {
const response = await fetch(`${API_BASE_URL}/routers?filter=${filter}`, { const response = await this.fetchWithAuth(`${API_BASE_URL}/routers?filter=${filter}`, {
method: 'GET', method: 'GET',
...DEFAULT_OPTIONS ...DEFAULT_OPTIONS
}); });
@ -112,7 +263,7 @@ class ApiService {
async getRouterById(id: number): Promise<RouterData | null> { async getRouterById(id: number): Promise<RouterData | null> {
try { try {
const response = await fetch(`${API_BASE_URL}/routers/${id}`, { const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
...DEFAULT_OPTIONS ...DEFAULT_OPTIONS
}); });
if (!response.ok) { if (!response.ok) {
@ -142,7 +293,7 @@ class ApiService {
last_seen: new Date().toISOString(), // Set current timestamp last_seen: new Date().toISOString(), // Set current timestamp
}; };
const response = await fetch(`${API_BASE_URL}/routers`, { const response = await this.fetchWithAuth(`${API_BASE_URL}/routers`, {
method: 'POST', method: 'POST',
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
body: JSON.stringify(backendData), body: JSON.stringify(backendData),
@ -174,7 +325,7 @@ class ApiService {
last_seen: new Date().toISOString() // Update timestamp on changes last_seen: new Date().toISOString() // Update timestamp on changes
}; };
const response = await fetch(`${API_BASE_URL}/routers/${id}`, { const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
method: 'PUT', method: 'PUT',
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
body: JSON.stringify(backendData), body: JSON.stringify(backendData),
@ -193,7 +344,7 @@ class ApiService {
async deleteRouter(id: number): Promise<boolean> { async deleteRouter(id: number): Promise<boolean> {
try { try {
const response = await fetch(`${API_BASE_URL}/routers/${id}`, { const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
method: 'DELETE', method: 'DELETE',
...DEFAULT_OPTIONS ...DEFAULT_OPTIONS
}); });
@ -209,7 +360,7 @@ class ApiService {
async getRoutersByFacility(facility: string): Promise<RouterData[]> { async getRoutersByFacility(facility: string): Promise<RouterData[]> {
try { try {
const response = await fetch( const response = await this.fetchWithAuth(
`${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`, `${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`,
{ ...DEFAULT_OPTIONS } { ...DEFAULT_OPTIONS }
); );
@ -226,7 +377,7 @@ class ApiService {
async checkApiStatus(): Promise<boolean> { async checkApiStatus(): Promise<boolean> {
try { try {
const response = await fetch(`${API_BASE_URL}/routers`, { const response = await this.fetchWithAuth(`${API_BASE_URL}/routers`, {
method: 'GET', method: 'GET',
...DEFAULT_OPTIONS ...DEFAULT_OPTIONS
}); });

View File

@ -0,0 +1,21 @@
// User Role enum
export type UserRole = 'admin' | 'operator' | 'viewer' | 'api';
// User Status enum
export type UserStatus = 'active' | 'locked' | 'disabled';
// User Interface
export interface User {
id: number;
name: string;
username: string;
email: string;
password_hash: string;
role: UserRole;
status: UserStatus;
failed_login_attempts: number;
last_login: Date | null;
password_changed_at: Date;
created_at: Date;
updated_at: Date;
}

View File

@ -19,7 +19,7 @@ export class AuthController {
const user = await this.service.validateUser(username, password); const user = await this.service.validateUser(username, password);
const { accessToken, refreshToken, sessionToken } = this.service.generateTokens(user); const { accessToken, refreshToken} = this.service.generateTokens(user);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days expiration const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days expiration
// Check for an active session for this user // Check for an active session for this user
@ -48,7 +48,6 @@ export class AuthController {
logger.info('Updating expired session.'); logger.info('Updating expired session.');
await this.service.updateUserSession(existingSession.refresh_token, { await this.service.updateUserSession(existingSession.refresh_token, {
refresh_token: refreshToken, refresh_token: refreshToken,
session_token: sessionToken,
expires_at: expiresAt expires_at: expiresAt
}); });
res.json({ res.json({
@ -69,7 +68,6 @@ export class AuthController {
// No session matches, Create a new user sessions // No session matches, Create a new user sessions
const userSessionDTO: Partial<CreateUserSessionDTO> = { const userSessionDTO: Partial<CreateUserSessionDTO> = {
user_id : user.id, user_id : user.id,
session_token : sessionToken,
refresh_token : refreshToken, refresh_token : refreshToken,
ip_address : req.ip, ip_address : req.ip,
user_agent : req.headers['user-agent'], user_agent : req.headers['user-agent'],
@ -135,12 +133,11 @@ export class AuthController {
role: userData.role as UserRole, role: userData.role as UserRole,
}; };
const { accessToken, refreshToken: newRefreshToken, sessionToken: newSessionToken } = const { accessToken, refreshToken: newRefreshToken} =
this.service.generateTokens(user); this.service.generateTokens(user);
const newExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days expiration const newExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days expiration
const userSessionData = { const userSessionData = {
session_token: newSessionToken,
refresh_token: newRefreshToken, refresh_token: newRefreshToken,
expires_at: newExpiresAt expires_at: newExpiresAt
} }
@ -176,7 +173,7 @@ export class AuthController {
return res.status(400).json({ message: "Refresh Token is required" }); return res.status(400).json({ message: "Refresh Token is required" });
} }
res.json({ message: 'Logged out successfully' }); res.status(200).json({ message: 'Logged out successfully' });
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
logger.error('Logout error:', { message: error.message, stack: error.stack }); logger.error('Logout error:', { message: error.message, stack: error.stack });

View File

@ -141,7 +141,6 @@ export class RouterRepository {
[routerId] [routerId]
); );
logger.info(`Containers for router ${routerId}:`, rows);
return rows as Container[]; return rows as Container[];
} catch (error) { } catch (error) {
logger.error(`Error fetching Containers for router ${routerId}:`, error); logger.error(`Error fetching Containers for router ${routerId}:`, error);

View File

@ -144,12 +144,11 @@ export class UserRepository {
const [result] = await pool.query( const [result] = await pool.query(
`INSERT INTO user_sessions ( `INSERT INTO user_sessions (
user_id, session_token, refresh_token, ip_address, user_id, refresh_token, ip_address,
user_agent, expires_at, created_at, last_activity user_agent, expires_at, created_at, last_activity
) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())`, ) VALUES (?, ?, ?, ?, ?, NOW(), NOW())`,
[ [
userSession.user_id, userSession.user_id,
userSession.session_token,
userSession.refresh_token, userSession.refresh_token,
userSession.ip_address, userSession.ip_address,
userSession.user_agent, userSession.user_agent,

View File

@ -19,7 +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: '15m' } //{ expiresIn: '30m' }
{ expiresIn: '1m' }
); );
}; };
@ -28,18 +29,18 @@ 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: '15m' } //{ expiresIn: '30m' }
{ expiresIn: '1m' }
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role, type: 'refresh' }, // Include a claim to distinguish token types { userId: user.id, username: user.username, role: user.role, type: 'refresh' }, // Include a claim to distinguish token types
process.env.JWT_SECRET as string, process.env.JWT_SECRET as string,
{ expiresIn: '7d' } // Longer expiry for refresh token { expiresIn: '7d' } // Longer expiry for refresh token
//{ expiresIn: '1m' }
); );
const sessionToken = crypto.randomBytes(40).toString('hex'); return { accessToken, refreshToken };
return { accessToken, refreshToken, sessionToken };
} }
// Validate the user by username and password // Validate the user by username and password
@ -64,7 +65,6 @@ export class AuthService {
async createUserSession (userSessionData: Partial<UserSession>) { async createUserSession (userSessionData: Partial<UserSession>) {
const requiredFields = [ const requiredFields = [
'user_id', 'user_id',
'session_token',
'refresh_token', 'refresh_token',
'ip_address', 'ip_address',
'user_agent', 'user_agent',
@ -103,7 +103,6 @@ export class AuthService {
async updateUserSession (refreshToken:string, userSessionData: Partial<UserSession>) { async updateUserSession (refreshToken:string, userSessionData: Partial<UserSession>) {
const requiredFields = [ const requiredFields = [
'session_token',
'refresh_token', 'refresh_token',
'expires_at' 'expires_at'
]; ];

View File

@ -16,9 +16,9 @@ export class SetupService {
const defaultUsers = [ const defaultUsers = [
{ name: 'API User', username: 'api_user', email: 'apiuser@ve.com', password: 'api_user@@124', role: 'api' }, { name: 'API User', username: 'api_user', email: 'apiuser@ve.com', password: 'api_user@@124', role: 'api' },
{ 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: '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: 'Maqbool Patel', username: 'maqbool', email: 'maqbool@ve.com', password: 'maqbool@@210', role: 'viewer' },
]; ];
const createdUsers = []; const createdUsers = [];

View File

@ -38,7 +38,6 @@ export interface UpdateUser {
export interface UserSession { export interface UserSession {
id: number; id: number;
user_id: number; user_id: number;
session_token: string;
refresh_token: string; refresh_token: string;
ip_address: string; ip_address: string;
user_agent: string | null; user_agent: string | null;
@ -50,7 +49,6 @@ export interface UserSession {
// Create User Session Interface // Create User Session Interface
export interface CreateUserSessionDTO { export interface CreateUserSessionDTO {
user_id: number; user_id: number;
session_token: string;
refresh_token: string; refresh_token: string;
ip_address: string; ip_address: string;
user_agent: string | null; user_agent: string | null;
@ -75,7 +73,6 @@ export interface UserWithSession {
}; };
session: { session: {
id: number; id: number;
session_token: string;
refresh_token: string; refresh_token: string;
ip_address: string; ip_address: string;
user_agent: string | null; user_agent: string | null;