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:
parent
ea7ac4cc72
commit
262307f17d
@ -106,7 +106,6 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
session_token VARCHAR(255) UNIQUE NOT NULL,
|
||||
refresh_token VARCHAR(255) NOT NULL,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
user_agent TEXT,
|
||||
@ -115,7 +114,6 @@ CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_user_session_user FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT unique_session_token UNIQUE(session_token),
|
||||
CONSTRAINT unique_refresh_token UNIQUE(refresh_token)
|
||||
);
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"lucide-react": "^0.294.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"router-dashboard": "file:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -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 DashboardLayout from './components/dashboard/DashboardLayout';
|
||||
import { useAuth } from './contexts/AuthContext';
|
||||
|
||||
function App() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
||||
|
||||
// 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);
|
||||
}, []);
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoggedIn ? <DashboardLayout /> : <Login />}
|
||||
</>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
isAuthenticated ? (
|
||||
<DashboardLayout />
|
||||
) : (
|
||||
<Navigate to="/login" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
isAuthenticated ? (
|
||||
<Navigate to="/" replace />
|
||||
) : (
|
||||
<Login />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,33 +1,83 @@
|
||||
import React, { useState } from 'react';
|
||||
import Header from './dashboard/Header';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { apiService } from '../services/api.service';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { setIsAuthenticated } = useAuth(); // Access the setIsAuthenticated function
|
||||
|
||||
const [username, setUsername] = 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();
|
||||
|
||||
// Mock authentication logic
|
||||
if (username === 'admin' && password === 'password') {
|
||||
localStorage.setItem('user', JSON.stringify({ accessToken: 'fake-token' }));
|
||||
window.location.href = '/dashboard';
|
||||
let hasError = false;
|
||||
|
||||
// Validate username and password
|
||||
if (!username.trim()) {
|
||||
setUsernameError('Username is required');
|
||||
hasError = true;
|
||||
} 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 (
|
||||
<div className="min-h-screen flex flex-col bg-gray-100">
|
||||
{/* Header */}
|
||||
<Header user={{}} onLogout={() => {}} />
|
||||
|
||||
{/* Login Content */}
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<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>
|
||||
<form onSubmit={handleLogin}>
|
||||
{/* Username Field */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||
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"
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
{usernameError && <div className="text-red-500 text-sm mt-1">{usernameError}</div>}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div className="mb-6">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
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"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
{passwordError && <div className="text-red-500 text-sm mt-1">{passwordError}</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
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-150"
|
||||
|
||||
@ -6,6 +6,8 @@ import RouterTable from './RouterTable';
|
||||
import RouterManagement from './pages/RouterManagement';
|
||||
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
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
@ -15,15 +17,36 @@ interface User {
|
||||
type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
||||
|
||||
const DashboardLayout: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated, setIsAuthenticated } = useAuth(); // Use AuthContext
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
||||
const [routers, setRouters] = useState<RouterData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [user] = useState<User>({
|
||||
name: 'John Doe',
|
||||
role: 'Administrator'
|
||||
});
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
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(() => {
|
||||
const fetchRouters = async () => {
|
||||
@ -64,9 +87,30 @@ const DashboardLayout: React.FC = () => {
|
||||
return () => clearInterval(interval);
|
||||
}, [activeFilter]);
|
||||
|
||||
const handleLogout = () => {
|
||||
// Handle logout functionality
|
||||
const handleLogout = async () => {
|
||||
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[]) => {
|
||||
@ -218,7 +262,7 @@ const DashboardLayout: React.FC = () => {
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<Header user={user} onLogout={handleLogout} />
|
||||
<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">
|
||||
{renderContent()}
|
||||
</main>
|
||||
|
||||
@ -9,11 +9,15 @@ interface User {
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
user: User;
|
||||
user: User | null; // Allow user to be null
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
|
||||
if (!user) {
|
||||
return null; // Don't render the header if the user is not available
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-sm">
|
||||
<div className="h-16 px-4 flex items-center justify-between">
|
||||
|
||||
@ -13,26 +13,29 @@ import {
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
label: string;
|
||||
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 [isPinned, setIsPinned] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ id: 'routers', label: 'Router Management', icon: RouterIcon },
|
||||
{ id: 'users', label: 'User Management', icon: Users },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings }
|
||||
const TABS_WITH_PERMISSIONS: Tab[] = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard, roles: ['admin', 'operator', 'viewer'] },
|
||||
{ id: 'routers', label: 'Router Management', icon: RouterIcon, roles: ['admin', 'operator'] },
|
||||
{ id: 'users', label: 'User Management', icon: Users, roles: ['admin'] },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings, roles: ['admin'] },
|
||||
];
|
||||
|
||||
const filteredTabs = TABS_WITH_PERMISSIONS.filter((tab) => tab.roles.includes(role));
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isPinned) {
|
||||
setIsCollapsed(false);
|
||||
@ -71,7 +74,6 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* Header with title and controls */}
|
||||
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<div>
|
||||
@ -79,7 +81,11 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||
<h2 className="text-sm font-semibold">System</h2>
|
||||
</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) && (
|
||||
<button
|
||||
onClick={togglePin}
|
||||
@ -90,7 +96,9 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||
>
|
||||
<Pin
|
||||
size={16}
|
||||
className={`transform transition-transform ${isPinned ? 'rotate-45' : ''}`}
|
||||
className={`transform transition-transform ${
|
||||
isPinned ? 'rotate-45' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
@ -99,18 +107,14 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||
className="p-1 rounded hover:bg-gray-700 transition-colors"
|
||||
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight size={16} />
|
||||
) : (
|
||||
<ChevronLeft size={16} />
|
||||
)}
|
||||
{isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Items */}
|
||||
<nav className="flex-1 p-2">
|
||||
{tabs.map(tab => {
|
||||
{filteredTabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
@ -119,9 +123,11 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
|
||||
transition-colors duration-200 group
|
||||
${activeTab === tab.id
|
||||
${
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-800'}
|
||||
: 'text-gray-300 hover:bg-gray-800'
|
||||
}
|
||||
`}
|
||||
title={isCollapsed ? tab.label : undefined}
|
||||
>
|
||||
@ -131,41 +137,6 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
41
router-dashboard/src/contexts/AuthContext.tsx
Normal file
41
router-dashboard/src/contexts/AuthContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -1,11 +1,17 @@
|
||||
// File: src/main.tsx
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter as Router } from 'react-router-dom'; // Import BrowserRouter for routing
|
||||
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(
|
||||
<React.StrictMode>
|
||||
<AuthProvider> {/* Wrap the App with AuthProvider to enable authentication context */}
|
||||
<Router> {/* Wrap the App with Router to enable routing */}
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@ -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
|
||||
const logResponse = (prefix: string, data: any) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@ -19,9 +48,131 @@ const logResponse = (prefix: string, data: any) => {
|
||||
};
|
||||
|
||||
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[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/routers?filter=${filter}`, {
|
||||
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers?filter=${filter}`, {
|
||||
method: 'GET',
|
||||
...DEFAULT_OPTIONS
|
||||
});
|
||||
@ -112,7 +263,7 @@ class ApiService {
|
||||
|
||||
async getRouterById(id: number): Promise<RouterData | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
|
||||
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
|
||||
...DEFAULT_OPTIONS
|
||||
});
|
||||
if (!response.ok) {
|
||||
@ -142,7 +293,7 @@ class ApiService {
|
||||
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',
|
||||
...DEFAULT_OPTIONS,
|
||||
body: JSON.stringify(backendData),
|
||||
@ -174,7 +325,7 @@ class ApiService {
|
||||
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',
|
||||
...DEFAULT_OPTIONS,
|
||||
body: JSON.stringify(backendData),
|
||||
@ -193,7 +344,7 @@ class ApiService {
|
||||
|
||||
async deleteRouter(id: number): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
|
||||
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
|
||||
method: 'DELETE',
|
||||
...DEFAULT_OPTIONS
|
||||
});
|
||||
@ -209,7 +360,7 @@ class ApiService {
|
||||
|
||||
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
const response = await this.fetchWithAuth(
|
||||
`${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`,
|
||||
{ ...DEFAULT_OPTIONS }
|
||||
);
|
||||
@ -226,7 +377,7 @@ class ApiService {
|
||||
|
||||
async checkApiStatus(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/routers`, {
|
||||
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers`, {
|
||||
method: 'GET',
|
||||
...DEFAULT_OPTIONS
|
||||
});
|
||||
|
||||
21
router-dashboard/src/types/user.ts
Normal file
21
router-dashboard/src/types/user.ts
Normal 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;
|
||||
}
|
||||
@ -19,7 +19,7 @@ export class AuthController {
|
||||
|
||||
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
|
||||
|
||||
// Check for an active session for this user
|
||||
@ -48,7 +48,6 @@ export class AuthController {
|
||||
logger.info('Updating expired session.');
|
||||
await this.service.updateUserSession(existingSession.refresh_token, {
|
||||
refresh_token: refreshToken,
|
||||
session_token: sessionToken,
|
||||
expires_at: expiresAt
|
||||
});
|
||||
res.json({
|
||||
@ -69,7 +68,6 @@ export class AuthController {
|
||||
// No session matches, Create a new user sessions
|
||||
const userSessionDTO: Partial<CreateUserSessionDTO> = {
|
||||
user_id : user.id,
|
||||
session_token : sessionToken,
|
||||
refresh_token : refreshToken,
|
||||
ip_address : req.ip,
|
||||
user_agent : req.headers['user-agent'],
|
||||
@ -135,12 +133,11 @@ export class AuthController {
|
||||
role: userData.role as UserRole,
|
||||
};
|
||||
|
||||
const { accessToken, refreshToken: newRefreshToken, sessionToken: newSessionToken } =
|
||||
const { accessToken, refreshToken: newRefreshToken} =
|
||||
this.service.generateTokens(user);
|
||||
const newExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days expiration
|
||||
|
||||
const userSessionData = {
|
||||
session_token: newSessionToken,
|
||||
refresh_token: newRefreshToken,
|
||||
expires_at: newExpiresAt
|
||||
}
|
||||
@ -176,7 +173,7 @@ export class AuthController {
|
||||
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) {
|
||||
const error = err as Error;
|
||||
logger.error('Logout error:', { message: error.message, stack: error.stack });
|
||||
|
||||
@ -141,7 +141,6 @@ export class RouterRepository {
|
||||
[routerId]
|
||||
);
|
||||
|
||||
logger.info(`Containers for router ${routerId}:`, rows);
|
||||
return rows as Container[];
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching Containers for router ${routerId}:`, error);
|
||||
|
||||
@ -144,12 +144,11 @@ export class UserRepository {
|
||||
|
||||
const [result] = await pool.query(
|
||||
`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
|
||||
) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
) VALUES (?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[
|
||||
userSession.user_id,
|
||||
userSession.session_token,
|
||||
userSession.refresh_token,
|
||||
userSession.ip_address,
|
||||
userSession.user_agent,
|
||||
|
||||
@ -19,7 +19,8 @@ export class AuthService {
|
||||
return jwt.sign(
|
||||
{ userId: user.id, username: user.username, role: user.role },
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '15m' }
|
||||
//{ expiresIn: '30m' }
|
||||
{ expiresIn: '1m' }
|
||||
);
|
||||
};
|
||||
|
||||
@ -28,18 +29,18 @@ export class AuthService {
|
||||
const accessToken = jwt.sign(
|
||||
{ userId: user.id, username: user.username, role: user.role },
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '15m' }
|
||||
//{ expiresIn: '30m' }
|
||||
{ expiresIn: '1m' }
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{ userId: user.id, username: user.username, role: user.role, type: 'refresh' }, // Include a claim to distinguish token types
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '7d' } // Longer expiry for refresh token
|
||||
//{ expiresIn: '1m' }
|
||||
);
|
||||
|
||||
const sessionToken = crypto.randomBytes(40).toString('hex');
|
||||
|
||||
return { accessToken, refreshToken, sessionToken };
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
// Validate the user by username and password
|
||||
@ -64,7 +65,6 @@ export class AuthService {
|
||||
async createUserSession (userSessionData: Partial<UserSession>) {
|
||||
const requiredFields = [
|
||||
'user_id',
|
||||
'session_token',
|
||||
'refresh_token',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
@ -103,7 +103,6 @@ export class AuthService {
|
||||
|
||||
async updateUserSession (refreshToken:string, userSessionData: Partial<UserSession>) {
|
||||
const requiredFields = [
|
||||
'session_token',
|
||||
'refresh_token',
|
||||
'expires_at'
|
||||
];
|
||||
|
||||
@ -16,9 +16,9 @@ export class SetupService {
|
||||
const defaultUsers = [
|
||||
{ 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: '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: 'Maqbool Patel', username: 'maqbool', email: 'maqbool@ve.com', password: 'maqbool@@210', role: 'viewer' },
|
||||
{ name: 'Reid McKenzie', username: 'reid', email: 'reid@ve.com', password: 'reid@@321', role: 'viewer' }
|
||||
];
|
||||
|
||||
const createdUsers = [];
|
||||
|
||||
@ -38,7 +38,6 @@ export interface UpdateUser {
|
||||
export interface UserSession {
|
||||
id: number;
|
||||
user_id: number;
|
||||
session_token: string;
|
||||
refresh_token: string;
|
||||
ip_address: string;
|
||||
user_agent: string | null;
|
||||
@ -50,7 +49,6 @@ export interface UserSession {
|
||||
// Create User Session Interface
|
||||
export interface CreateUserSessionDTO {
|
||||
user_id: number;
|
||||
session_token: string;
|
||||
refresh_token: string;
|
||||
ip_address: string;
|
||||
user_agent: string | null;
|
||||
@ -75,7 +73,6 @@ export interface UserWithSession {
|
||||
};
|
||||
session: {
|
||||
id: number;
|
||||
session_token: string;
|
||||
refresh_token: string;
|
||||
ip_address: string;
|
||||
user_agent: string | null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user