Added code to insert initial users. Added backend code to generate access, refresh and session token using jwt. Added code to handle auth at backend side. Added login page at frontend side.

This commit is contained in:
shankar 2024-12-07 22:18:28 +05:30
parent dfa974fe6b
commit 2047e4bbf6
29 changed files with 1141 additions and 83 deletions

View File

@ -23,50 +23,6 @@ CREATE TABLE IF NOT EXISTS routers (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
); );
-- Users and Authentication tables
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin', 'operator', 'viewer') NOT NULL DEFAULT 'viewer',
status ENUM('active', 'locked', 'disabled') NOT NULL DEFAULT 'active',
failed_login_attempts INT NOT NULL DEFAULT 0,
last_login TIMESTAMP NULL,
password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT valid_email CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
CONSTRAINT valid_username CHECK (username REGEXP '^[A-Za-z0-9_-]{3,50}$')
);
-- User-Router access permissions
CREATE TABLE IF NOT EXISTS user_router_access (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
router_id INT NOT NULL,
can_view BOOLEAN NOT NULL DEFAULT TRUE,
can_manage BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_user_router_access (user_id, router_id)
);
-- Session management
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,
expires_at TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL 24 HOUR),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT unique_session_token UNIQUE(session_token),
CONSTRAINT unique_refresh_token UNIQUE(refresh_token)
);
-- Container status table -- Container status table
CREATE TABLE IF NOT EXISTS container_status ( CREATE TABLE IF NOT EXISTS container_status (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY, id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
@ -127,5 +83,69 @@ CREATE TABLE IF NOT EXISTS status_category (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
); );
-- User authentication and authorization
-- Users table
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin', 'operator', 'viewer', 'api') NOT NULL DEFAULT 'viewer',
status ENUM('active', 'locked', 'disabled') NOT NULL DEFAULT 'active',
failed_login_attempts INT NOT NULL DEFAULT 0,
last_login TIMESTAMP NULL,
password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT valid_email CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
CONSTRAINT valid_username CHECK (username REGEXP '^[A-Za-z0-9_-]{3,50}$')
);
-- Session management
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,
expires_at TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL 24 HOUR),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
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)
);
-- Authentication audit log
CREATE TABLE IF NOT EXISTS auth_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
event_type VARCHAR(50) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT,
details JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_auth_log_user FOREIGN KEY (user_id)
REFERENCES users(id) ON DELETE SET NULL
);
-- User-Router access permissions
CREATE TABLE IF NOT EXISTS user_router_access (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
router_id INT NOT NULL,
can_view BOOLEAN NOT NULL DEFAULT TRUE,
can_manage BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_user_router_access_user FOREIGN KEY (user_id)
REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_user_router_access_router FOREIGN KEY (router_id)
REFERENCES routers(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_router_access (user_id, router_id)
);
-- Additional history and settings tables as needed... -- Additional history and settings tables as needed...

View File

@ -1,7 +1,21 @@
import React, { useState, useEffect } from 'react';
import Login from './components/Login';
import DashboardLayout from './components/dashboard/DashboardLayout'; import DashboardLayout from './components/dashboard/DashboardLayout';
function App() { function App() {
return <DashboardLayout />; 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);
}, []);
return (
<>
{isLoggedIn ? <DashboardLayout /> : <Login />}
</>
);
} }
export default App; export default App;

View File

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import Header from './dashboard/Header';
const Login: React.FC = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleLogin = (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';
} else {
setError('Invalid username or password');
}
};
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}>
<div className="mb-4">
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
Username
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
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"
/>
</div>
<div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
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"
/>
</div>
{error && (
<div className="text-red-500 text-sm mb-4">{error}</div>
)}
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-150"
>
Login
</button>
</form>
</div>
</div>
</div>
);
};
export default Login;

View File

@ -12,7 +12,7 @@ DB_NAME=ve_router_db
DB_CONNECTION_LIMIT=10 DB_CONNECTION_LIMIT=10
# Authentication Configuration # Authentication Configuration
JWT_SECRET=your-super-secure-jwt-secret-key JWT_SECRET=VE_Router_JWT_Secret_2024@Key
JWT_EXPIRES_IN=1d JWT_EXPIRES_IN=1d
SALT_ROUNDS=10 SALT_ROUNDS=10

View File

@ -17,7 +17,9 @@
"express": "^4.18.2", "express": "^4.18.2",
"mysql2": "^3.2.0", "mysql2": "^3.2.0",
"ve-router-backend": "file:", "ve-router-backend": "file:",
"winston": "^3.16.0" "winston": "^3.16.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
@ -28,6 +30,8 @@
"eslint": "^8.37.0", "eslint": "^8.37.0",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.0.3" "typescript": "^5.0.3",
"@types/bcryptjs": "^2.4.3",
"@types/jsonwebtoken": "^9.0.2"
} }
} }

View File

@ -5,6 +5,8 @@ import config from './config/config';
import routes from './routes'; import routes from './routes';
import { errorHandler } from './middleware'; import { errorHandler } from './middleware';
import logger from './utils/logger'; import logger from './utils/logger';
import pool from './config/db';
import { SetupService } from './services';
const app = express(); const app = express();
@ -52,6 +54,19 @@ app.use((req, res) => {
}); });
}); });
const setupDefaultUsers = async () => {
try {
const setupService = new SetupService(pool);
await setupService.createDefaultUsers();
logger.info('Default users created successfully.');
} catch (error) {
logger.error('Error creating default users:', error);
}
};
// Call setupDefaultUsers during app initialization
setupDefaultUsers();
const port = config.server.port || 3000; const port = config.server.port || 3000;
app.listen(port, () => { app.listen(port, () => {
logger.info(`Server running on port ${port} in ${config.env} mode`); logger.info(`Server running on port ${port} in ${config.env} mode`);

View File

@ -0,0 +1,183 @@
// src/controllers/AuthController.ts
import { Request, Response} from 'express';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
import { AuthService} from '../services';
import { CreateUserSessionDTO, UserRole } from '@/types/user';
export class AuthController {
private service: AuthService;
constructor(pool: Pool) {
this.service = new AuthService(pool);
}
getAuthToken = async (req: Request, res: Response) => {
try {
const { username, password } = req.body;
logger.info(`Login attempt for: ${username}`);
const user = await this.service.validateUser(username, password);
const { accessToken, refreshToken, sessionToken } = 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
const existingSession = username === 'api_user'
? await this.service.getUserSessionByIp(user.id, req.ip?? '')
: await this.service.getUserSessionByUserAndAgent(user.id, req.headers['user-agent']?? '');
if (existingSession) {
if (existingSession.expires_at > new Date()) {
// Active session found
logger.info('Reusing existing session.');
const newAccessToken = this.service.generateAccessToken(user);
res.json({
accessToken: newAccessToken,
refreshToken: existingSession.refresh_token, // Reuse
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
},
});
return;
} else {
// Expired session found, refresh it
logger.info('Updating expired session.');
await this.service.updateUserSession(existingSession.refresh_token, {
refresh_token: refreshToken,
session_token: sessionToken,
expires_at: expiresAt
});
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
},
});
return;
}
}
// 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'],
expires_at : expiresAt
};
await this.service.createUserSession(userSessionDTO);
// Reset login attempts
user.failed_login_attempts = 0;
user.last_login = new Date();
await this.service.updateUser(user.id, user);
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
name: user.name,
username: user.username,
email: user.email,
role: user.role,
},
});
} catch (err) {
const error = err as Error;
if (error instanceof Error && error.message.includes('Invalid credentials')) {
logger.error(`Auth Error: ${error.message}`);
return res.status(401).json({ message: error.message });
}
// Default to API error handling
logger.error('Auth error:', { message: error.message, stack: error.stack });
return res.status(500).json({ message: 'Internal Server Error' });
}
};
login = async (req: Request, res: Response) => {
this.getAuthToken(req, res);
};
refreshToken = async (req: Request, res: Response): Promise<void> => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
res.status(401).json({ message: 'Refresh token required' });
return;
}
const userData = await this.service.getUserAndSessionByRefreshToken(refreshToken);
if (!userData) {
res.status(401).json({ message: 'Invalid refresh token' });
return;
}
const user = {
id: userData.id,
username: userData.username,
role: userData.role as UserRole,
};
const { accessToken, refreshToken: newRefreshToken, sessionToken: newSessionToken } =
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
}
//update new refresh token
await this.service.updateUserSession(refreshToken, userSessionData);
res.json({
accessToken,
refreshToken: newRefreshToken,
user: {
id: user.id,
username: user.username,
role: user.role,
},
});
} catch (err) {
const error = err as Error;
logger.error('Refresh Token update error:', { message: error.message, stack: error.stack });
res.status(500).json({ message: 'Refresh Token update failed' });
}
};
logout = async (req: Request, res: Response) => {
try {
const { refreshToken } = req.body;
if (refreshToken) {
await this.service.deleteUserSession(refreshToken);
} else {
return res.status(400).json({ message: "Refresh Token is required" });
}
res.json({ message: 'Logged out successfully' });
} catch (err) {
const error = err as Error;
logger.error('Logout error:', { message: error.message, stack: error.stack });
res.status(500).json({ message: 'Internal server error' });
}
};
}

View File

@ -1,39 +1,17 @@
// src/controllers/DicomStudyController.ts // src/controllers/DicomStudyController.ts
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { DicomStudyService } from '../services/DicomStudyService'; import { DicomStudyService, CommonService} from '../services';
import logger from '../utils/logger'; import logger from '../utils/logger';
import { Pool } from 'mysql2/promise'; import { Pool } from 'mysql2/promise';
interface ApiError {
message: string;
code?: string;
stack?: string;
}
export class DicomStudyController { export class DicomStudyController {
private service: DicomStudyService; private service: DicomStudyService;
private commonService: CommonService;
constructor(pool:Pool) { constructor(pool:Pool) {
this.service = new DicomStudyService(pool); this.service = new DicomStudyService(pool);
} this.commonService = new CommonService();
private handleError(error: unknown, message: string): ApiError {
const apiError: ApiError = {
message: message
};
if (error instanceof Error) {
logger.error(`${message}: ${error.message}`);
if (process.env.NODE_ENV === 'development') {
apiError.message = error.message;
apiError.stack = error.stack;
}
} else {
logger.error(`${message}: Unknown error type`, error);
}
return apiError;
} }
getAllStudies = async (req: Request, res: Response, next: NextFunction) => { getAllStudies = async (req: Request, res: Response, next: NextFunction) => {
@ -41,7 +19,7 @@ export class DicomStudyController {
const studies = await this.service.getAllStudies(); const studies = await this.service.getAllStudies();
res.json(studies); res.json(studies);
} catch (error) { } catch (error) {
const apiError = this.handleError(error, 'Failed to fetch studies'); const apiError = this.commonService.handleError(error, 'Failed to fetch studies');
res.status(500).json({ error: apiError }); res.status(500).json({ error: apiError });
} }
}; };
@ -60,7 +38,7 @@ export class DicomStudyController {
res.json(study); res.json(study);
} catch (error) { } catch (error) {
const apiError = this.handleError(error, `Failed to fetch study ${req.params.id}`); const apiError = this.commonService.handleError(error, `Failed to fetch study ${req.params.id}`);
res.status(500).json({ error: apiError }); res.status(500).json({ error: apiError });
} }
}; };
@ -75,7 +53,7 @@ export class DicomStudyController {
const studies = await this.service.getStudiesByRouterId(routerId); const studies = await this.service.getStudiesByRouterId(routerId);
res.json(studies); res.json(studies);
} catch (error) { } catch (error) {
const apiError = this.handleError(error, `Failed to fetch studies for router ${req.params.routerId}`); const apiError = this.commonService.handleError(error, `Failed to fetch studies for router ${req.params.routerId}`);
// If router not found, return 404 // If router not found, return 404
if (error instanceof Error && error.message.includes('Invalid router_id')) { if (error instanceof Error && error.message.includes('Invalid router_id')) {
return res.status(404).json({ error: 'Router not found' }); return res.status(404).json({ error: 'Router not found' });
@ -118,7 +96,7 @@ export class DicomStudyController {
const study = await this.service.createStudy(req.body); const study = await this.service.createStudy(req.body);
res.status(201).json(study); res.status(201).json(study);
} catch (error) { } catch (error) {
const apiError = this.handleError(error, 'Failed to create study'); const apiError = this.commonService.handleError(error, 'Failed to create study');
// Handle specific error cases // Handle specific error cases
if (error instanceof Error) { if (error instanceof Error) {
@ -154,7 +132,7 @@ export class DicomStudyController {
res.json(study); res.json(study);
} catch (error) { } catch (error) {
const apiError = this.handleError(error, `Failed to update study ${req.params.id}`); const apiError = this.commonService.handleError(error, `Failed to update study ${req.params.id}`);
res.status(500).json({ error: apiError }); res.status(500).json({ error: apiError });
} }
}; };
@ -173,7 +151,7 @@ export class DicomStudyController {
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
const apiError = this.handleError(error, `Failed to delete study ${req.params.id}`); const apiError = this.commonService.handleError(error, `Failed to delete study ${req.params.id}`);
res.status(500).json({ error: apiError }); res.status(500).json({ error: apiError });
} }
}; };
@ -204,7 +182,7 @@ export class DicomStudyController {
res.json(studies); res.json(studies);
} catch (error) { } catch (error) {
const apiError = this.handleError(error, 'Failed to search studies'); const apiError = this.commonService.handleError(error, 'Failed to search studies');
res.status(500).json({ error: apiError }); res.status(500).json({ error: apiError });
} }
}; };

View File

@ -10,7 +10,7 @@ import logger from '../utils/logger';
export class RouterController { export class RouterController {
private service: RouterService; private service: RouterService;
private dicomStudyService: DicomStudyService; private dicomStudyService: DicomStudyService;
private utilityService: UtilityService private utilityService: UtilityService;
constructor(pool: Pool) { constructor(pool: Pool) {
this.service = new RouterService(pool); this.service = new RouterService(pool);

View File

@ -0,0 +1,23 @@
import { Request, Response} from 'express';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
import { SetupService } from '../services';
export class SetupController {
private service: SetupService;
constructor(pool: Pool) {
this.service = new SetupService(pool);
}
createInitialUser = async (req: Request, res: Response) => {
try {
const user = await this.service.createDefaultUsers();
res.status(201).json({ message: 'Initial user created successfully', user });
} catch (err) {
const error = err as Error
res.status(500).json({ message: error.message });
}
};
};

View File

@ -0,0 +1,56 @@
// src/controllers/AuthController.ts
import { Request, Response} from 'express';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
import { UserService } from '../services';
export class UserController {
private service: UserService;
constructor(pool: Pool) {
this.service = new UserService(pool);
}
getAllUsers = async (req: Request, res: Response) => {
};
getUserById = async (req: Request, res: Response) => {
try {
const { username} = req.body;
logger.info(`Get profile for: ${username}`);
const user = await this.service.getUserByUsername(username);
if (!user) {
return res.status(404).json({ error: 'Invalid user' });
}
res.json({
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
},
});
} catch (err) {
const error = err as Error;
logger.error('Get profile error:', { message: error.message, stack: error.stack });
res.status(500).json({ message: 'Internal server error' });
}
};
createUser = async (req: Request, res: Response) => {
};
updateUser = async (req: Request, res: Response) => {
};
deleteUser = async (req: Request, res: Response) => {
};
}

View File

@ -1,3 +1,4 @@
export * from './RouterController'; export * from './RouterController';
export * from './DicomStudyController'; export * from './DicomStudyController';
export * from './AuthController';
// Add more controller exports as needed // Add more controller exports as needed

View File

@ -0,0 +1,71 @@
// backend/middleware/auth.ts
import logger from '../utils/logger';
import { Request, Response, NextFunction } from 'express';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { Pool } from 'mysql2/promise';
// Extend Request to include a user property
declare module 'express-serve-static-core' {
interface Request {
user?: { id: number; username: string; role: string } | null;
}
}
export const authMiddleware = (pool: Pool) => async (req: Request, res: Response, next: NextFunction) => {
try {
logger.info("Auth middleware triggered");
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
logger.warn("Authorization header missing or invalid:", { authHeader });
return res.status(401).json({ message: 'Authorization header missing or invalid' });
}
const token = authHeader.split(' ')[1];
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error("JWT_SECRET is not set in environment variables");
}
let decoded: JwtPayload;
try {
decoded = jwt.verify(token, jwtSecret) as JwtPayload;
} catch (error: unknown) {
logger.error("JWT verification failed:", error);
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).json({
message: 'Access token expired',
code: 'TOKEN_EXPIRED',
});
}
return res.status(401).json({ message: 'Invalid token', code: 'INVALID_TOKEN' });
}
const userId = decoded.userId;
if (!userId || typeof userId !== 'number') {
logger.warn("Invalid or missing userId in token payload:", { decoded });
return res.status(401).json({ message: 'Invalid token payload' });
}
try {
const [rows] = await pool.execute(
'SELECT id, username, role FROM users WHERE id = ? AND status = "active"',
[userId]
);
const users = Array.isArray(rows) ? rows : [];
if (users.length === 0) {
logger.warn("User not found or inactive:", { userId });
return res.status(401).json({ message: 'User not found or inactive' });
}
req.user = users[0] as { id: number; username: string; role: string };
next();
} catch (dbError) {
logger.error("Database query failed:", dbError);
return res.status(500).json({ message: 'Database query error' });
}
} catch (error: unknown) {
logger.error('Error in authMiddleware:', error);
res.status(500).json({ message: 'Internal server error' });
}
};

View File

@ -1,2 +1,3 @@
export * from './errorHandler'; export * from './errorHandler';
export * from './auth';
// Add more middleware exports as needed // Add more middleware exports as needed

View File

@ -0,0 +1,217 @@
// src/repositories/UserRepository.ts
import { User, UpdateUser, UserWithSession, UserSession, UserStatus, CreateUserSessionDTO} from '../types/user';
import pool from '../config/db';
import { RowDataPacket, ResultSetHeader } from 'mysql2';
import logger from '../utils/logger';
import { Pool } from 'mysql2/promise';
export class UserRepository {
constructor(private pool: Pool) {} // Modified constructor
async findById(id: number): Promise<User | null> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM users WHERE id = ?',
[id]
);
if (!rows.length) return null;
return rows[0] as User;
}
async findByUsername(username: string): Promise<User | null> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM users WHERE username = ? AND status = "active"',
[username]
);
if (!rows.length) return null;
return rows[0] as User;
}
async findUserByUsernameOrEmail(username: string, email: string): Promise<User | null> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM users WHERE (username = ? OR email = ?) AND status = "active"',
[username, email]
);
if (!rows.length) return null;
return rows[0] as User;
}
async create(user: Partial<User>): Promise<User> {
const [result] = await pool.query<ResultSetHeader>(
`INSERT INTO users (
name, username, email, password_hash, role
) VALUES (?, ?, ?, ?, ?)`,
[
user.name,
user.username,
user.email,
user.password_hash,
user.role
]
);
return this.findById(result.insertId) as Promise<User>;
}
async update(id: number, userData: UpdateUser): Promise<User | null> {
try {
// Build update query dynamically based on provided fields
const updateFields: string[] = [];
const updateValues: any[] = [];
Object.entries(userData).forEach(([key, value]) => {
if (value !== undefined) {
updateFields.push(`${key} = ?`);
updateValues.push(value);
}
});
if (updateFields.length > 0) {
// Add updated_at timestamp
updateFields.push('updated_at = CURRENT_TIMESTAMP');
// Add id for WHERE clause
updateValues.push(id);
await pool.query(`
UPDATE users
SET ${updateFields.join(', ')}
WHERE id = ?
`, updateValues);
}
// Return updated study
return await this.findById(id);
} catch (error) {
logger.error('Error updating user:', error);
throw new Error('Failed to update user');
}
}
async findUserSessionById(id: number): Promise<UserSession | null> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM user_sessions WHERE id = ?',
[id]
);
if (!rows.length) return null;
return rows[0] as UserSession;
}
async getUserSessionByIp(userId: number, ipAdress: string): Promise<UserSession | null> {
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT *
FROM user_sessions
WHERE user_id = ?
AND ip_address = ?
ORDER BY expires_at DESC
LIMIT 1`,
[userId, ipAdress]
);
return rows.length > 0 ? rows[0] as UserSession : null;
}
async getUserSessionByUserAndAgent(userId: number, userAgent: string): Promise<UserSession | null> {
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT *
FROM user_sessions
WHERE user_id = ?
AND user_agent = ?
ORDER BY expires_at DESC
LIMIT 1`,
[userId, userAgent]
);
return rows.length > 0 ? rows[0] as UserSession : null;
}
async getUserAndSessionByRefreshToken(refreshToken: string): Promise<User | null> {
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT users.*
FROM user_sessions
JOIN users ON user_sessions.user_id = users.id
WHERE refresh_token = ? AND expires_at > NOW() AND users.status = "active"`,
[refreshToken]
);
return rows.length > 0 ? (rows[0] as User) : null;
}
async createUserSession(userSession: Partial<UserSession>): Promise<UserSession> {
try {
const [result] = await pool.query(
`INSERT INTO user_sessions (
user_id, session_token, refresh_token, ip_address,
user_agent, expires_at, created_at, last_activity
) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())`,
[
userSession.user_id,
userSession.session_token,
userSession.refresh_token,
userSession.ip_address,
userSession.user_agent,
userSession.expires_at,
]
);
// Get the created study with the correct router_id format
const insertId = (result as any).insertId;
return await this.findUserSessionById(insertId) as UserSession;
} catch (error) {
logger.error('Error creating User session:', error);
throw new Error('Failed to create User session');
}
}
async updateUserSession(refreshToken: string, userSessionData: Partial<UserSession>): Promise<boolean> {
try {
// Build update query dynamically based on provided fields
const updateFields: string[] = [];
const updateValues: any[] = [];
Object.entries(userSessionData).forEach(([key, value]) => {
if (value !== undefined) {
updateFields.push(`${key} = ?`);
updateValues.push(value);
}
});
if (updateFields.length > 0) {
// Add updated_at timestamp
updateFields.push('last_activity = CURRENT_TIMESTAMP');
// Add id for WHERE clause
updateValues.push(refreshToken);
const [result] = await pool.query<ResultSetHeader>(`
UPDATE user_sessions
SET ${updateFields.join(', ')}
WHERE refresh_token = ?
`, updateValues);
// Return true if at least one row was affected
return result.affectedRows > 0;
}
// Return updated study
return false;
} catch (error) {
logger.error('Error updating user sessions:', error);
throw new Error('Failed to update user sessions');
}
}
async deleteUserSession(refreshToken: string): Promise<boolean> {
const [result] = await pool.query<ResultSetHeader>(
'DELETE FROM user_sessions WHERE refresh_token = ?',
[refreshToken]
);
return result.affectedRows > 0;
}
}

View File

@ -0,0 +1,21 @@
// src/routes/router.routes.ts
import { Router } from 'express';
import pool from '../config/db'; // If using default export
import { AuthController } from '../controllers/AuthController';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const authController = new AuthController(pool);
router.post('/login', authController.login);
router.post('/token', authController.getAuthToken);
router.post('/refresh-token', authController.refreshToken);
// Protected routes
router.use(authMiddleware(pool));
router.post('/logout', authController.logout);
// Export the router
export default router;

View File

@ -4,6 +4,7 @@ import express from 'express';
import { DicomStudyController } from '../controllers/DicomStudyController'; import { DicomStudyController } from '../controllers/DicomStudyController';
import logger from '../utils/logger'; import logger from '../utils/logger';
import pool from '../config/db'; // If using default export import pool from '../config/db'; // If using default export
import { authMiddleware } from '../middleware/auth';
const router = express.Router(); const router = express.Router();
const dicomStudyController = new DicomStudyController(pool); const dicomStudyController = new DicomStudyController(pool);
@ -22,6 +23,9 @@ router.get('/test', (req, res) => {
res.json({ message: 'DICOM routes are working' }); res.json({ message: 'DICOM routes are working' });
}); });
// Protected routes
router.use(authMiddleware(pool));
router.post('/', (req, res, next) => { router.post('/', (req, res, next) => {
logger.debug('POST / route hit with body:', req.body); logger.debug('POST / route hit with body:', req.body);
dicomStudyController.createStudy(req, res, next); dicomStudyController.createStudy(req, res, next);

View File

@ -3,6 +3,8 @@ import { Router } from 'express';
import routerRoutes from './router.routes'; import routerRoutes from './router.routes';
import dicomRoutes from './dicom.routes'; import dicomRoutes from './dicom.routes';
import logger from '../utils/logger'; import logger from '../utils/logger';
import authRoutes from './auth.routes';
import userRoutes from './user.routes';
const router = Router(); const router = Router();
@ -10,9 +12,13 @@ const router = Router();
logger.info('Registering routes:'); logger.info('Registering routes:');
logger.info('- /routers -> router routes'); logger.info('- /routers -> router routes');
logger.info('- /studies -> dicom routes'); logger.info('- /studies -> dicom routes');
logger.info('- /auth -> auth routes');
logger.info('- /users -> user routes');
router.use('/routers', routerRoutes); router.use('/routers', routerRoutes);
router.use('/studies', dicomRoutes); router.use('/studies', dicomRoutes);
router.use('/auth', authRoutes);
router.use('/users', userRoutes);
// Debug middleware to log incoming requests // Debug middleware to log incoming requests
router.use((req, res, next) => { router.use((req, res, next) => {

View File

@ -2,6 +2,7 @@
import { Router } from 'express'; import { Router } from 'express';
import pool from '../config/db'; // If using default export import pool from '../config/db'; // If using default export
import { RouterController } from '../controllers/RouterController'; import { RouterController } from '../controllers/RouterController';
import { authMiddleware } from '../middleware/auth';
@ -9,6 +10,9 @@ import { RouterController } from '../controllers/RouterController';
const router = Router(); const router = Router();
const controller = new RouterController(pool); const controller = new RouterController(pool);
// Protected routes
router.use(authMiddleware(pool));
router.put('/vms', async (req, res, next) => { router.put('/vms', async (req, res, next) => {
console.log('Route handler: /vms endpoint hit'); console.log('Route handler: /vms endpoint hit');
console.log('Query params:', req.query); console.log('Query params:', req.query);

View File

@ -0,0 +1,10 @@
import { Router } from 'express';
import pool from '../config/db'; // If using default export
import { SetupController } from '../controllers/SetupController';
const router = Router();
const controller = new SetupController(pool);
router.post('/setup', controller.createInitialUser);
export default router;

View File

@ -0,0 +1,19 @@
// src/routes/router.routes.ts
import { Router } from 'express';
import pool from '../config/db'; // If using default export
import { UserController } from '../controllers/UserController';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const controller = new UserController(pool);
// Protected routes
router.use(authMiddleware(pool));
router.get('/', controller.getAllUsers);
router.get('/:id', controller.getUserById);
router.post('/', controller.createUser);
router.put('/:id', controller.updateUser);
router.delete('/:id', controller.deleteUser);
export default router;

View File

@ -0,0 +1,122 @@
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
import { UserService } from '../services';
import { User, UserSession, CreateUserSessionDTO, UpdateUser, UserWithSession } from '../types/user';
export class AuthService {
private userService: UserService;
constructor(pool: Pool) {
this.userService = new UserService(pool);
}
// Generate JWT token
generateAccessToken(user: Partial<User>) {
return jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET as string,
{ expiresIn: '15m' }
);
};
// Generate JWT tokens
generateTokens(user: Partial<User>) {
const accessToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET as string,
{ expiresIn: '15m' }
);
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
);
const sessionToken = crypto.randomBytes(40).toString('hex');
return { accessToken, refreshToken, sessionToken };
}
// Validate the user by username and password
async validateUser(username: string, password: string): Promise<User> {
const user = await this.userService.getUserByUsername(username);
if (!user) {
throw new Error('Invalid credentials'); // Throw custom error
}
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) {
throw new Error('Invalid credentials'); // Throw custom error
}
return user; // Return the valid user
};
async getUserById (id: number): Promise<User | null> {
return await this.userService.getUserById(id);
};
async createUserSession (userSessionData: Partial<UserSession>) {
const requiredFields = [
'user_id',
'session_token',
'refresh_token',
'ip_address',
'user_agent',
'expires_at'
];
for (const field of requiredFields) {
// Check for undefined or null only (allow empty strings)
if (userSessionData[field as keyof UserSession] == null) {
throw new Error(`Missing required field: ${field}`);
}
}
logger.info('Creating new user session', { userSessionData });
const userSession = await this.userService.createUserSession(userSessionData);
};
async updateUser (userId: number, user: UpdateUser) {
this.userService.updateUser(userId, user);
};
async getUserSessionByIp (userId: number, ipAdress: string): Promise<UserSession | null> {
return await this.userService.getUserSessionByIp(userId, ipAdress);
};
async getUserSessionByUserAndAgent (userId: number, userAgent: string): Promise<UserSession | null> {
return await this.userService.getUserSessionByUserAndAgent(userId, userAgent);
};
async getUserAndSessionByRefreshToken (refreshToken: string): Promise<User | null> {
return this.userService.getUserAndSessionByRefreshToken(refreshToken);
};
async deleteUserSession (refreshToken: string) {
this.userService.deleteUserSession(refreshToken);
};
async updateUserSession (refreshToken:string, userSessionData: Partial<UserSession>) {
const requiredFields = [
'session_token',
'refresh_token',
'expires_at'
];
for (const field of requiredFields) {
// Check for undefined or null only (allow empty strings)
if (userSessionData[field as keyof UserSession] == null) {
throw new Error(`Missing required field: ${field}`);
}
}
logger.info('Updating user session', { userSessionData });
const userSession = await this.userService.updateUserSession(refreshToken, userSessionData);
};
}

View File

@ -0,0 +1,25 @@
import logger from '../utils/logger';
import { ApiError } from '@/types/error';
export class CommonService {
handleError(error: unknown, message: string): ApiError {
const apiError: ApiError = {
message: message
};
if (error instanceof Error) {
logger.error(`${message}: ${error.message}`);
if (process.env.NODE_ENV === 'development') {
apiError.message = error.message;
apiError.stack = error.stack;
}
} else {
logger.error(`${message}: Unknown error type`, error);
}
return apiError;
}
}

View File

@ -0,0 +1,45 @@
import { UserRepository } from '../repositories/UserRepository';
import { User, UserRole } from '../types/user';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
import bcrypt from 'bcryptjs';
export class SetupService {
private repository: UserRepository;
constructor(pool: Pool) {
this.repository = new UserRepository(pool);
}
createDefaultUsers = async () => {
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: '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' },
];
const createdUsers = [];
for (const user of defaultUsers) {
// Check if the user already exists
const existingUser = await this.repository.findUserByUsernameOrEmail(user.username, user.email);
if (!existingUser) {
const hashedPassword = await bcrypt.hash(user.password, 10);
const newUser = await this.repository.create({
name: user.name,
username: user.username,
email: user.email,
password_hash: hashedPassword,
role: user.role as UserRole
});
createdUsers.push(newUser);
}
}
return createdUsers;
};
};

View File

@ -0,0 +1,50 @@
// src/services/UserService.ts
import { UserRepository } from '../repositories/UserRepository';
import { User, UpdateUser, UserSession, CreateUserSessionDTO, UserWithSession} from '../types/user';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
export class UserService {
private repository: UserRepository;
constructor(pool: Pool) {
this.repository = new UserRepository(pool);
}
async getUserByUsername(username: string): Promise<User | null> {
return this.repository.findByUsername(username);
}
async updateUser(id: number, user: UpdateUser): Promise<User | null> {
return this.repository.update(id, user);
}
async getUserById(id: number): Promise<User | null> {
return await this.repository.findById(id);
}
async getUserSessionByIp (userId: number, ipAdress: string): Promise<UserSession | null> {
return await this.repository.getUserSessionByIp(userId, ipAdress);
}
async getUserSessionByUserAndAgent (userId: number, userAgent: string): Promise<UserSession | null> {
return await this.repository.getUserSessionByUserAndAgent(userId, userAgent);
}
async createUserSession(userSessionDTO: Partial<UserSession>): Promise<UserSession> {
return this.repository.createUserSession(userSessionDTO);
}
async getUserAndSessionByRefreshToken (refreshToken: string): Promise<User | null> {
return this.repository.getUserAndSessionByRefreshToken(refreshToken);
};
async updateUserSession (refreshToken:string, userSessionData: Partial<UserSession>) {
return this.repository.updateUserSession(refreshToken, userSessionData);
}
async deleteUserSession(refreshToken: string): Promise<boolean> {
return this.repository.deleteUserSession(refreshToken);
}
}

View File

@ -1,4 +1,8 @@
export * from './RouterService'; export * from './RouterService';
export * from './DicomStudyService'; export * from './DicomStudyService';
export * from './UtilityService'; export * from './UtilityService';
export * from './AuthService';
export * from './UserService';
export * from './CommonService';
export * from './SetupService';
// Add more service exports as needed // Add more service exports as needed

View File

@ -0,0 +1,6 @@
export interface ApiError {
message: string;
code?: string;
stack?: string;
}

View File

@ -0,0 +1,86 @@
// 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;
}
// Update User Interface
export interface UpdateUser {
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;
}
// User Session Interface
export interface UserSession {
id: number;
user_id: number;
session_token: string;
refresh_token: string;
ip_address: string;
user_agent: string | null;
expires_at: Date;
created_at: Date;
last_activity: Date;
}
// Create User Session Interface
export interface CreateUserSessionDTO {
user_id: number;
session_token: string;
refresh_token: string;
ip_address: string;
user_agent: string | null;
expires_at: Date;
created_at: Date;
last_activity: Date;
}
// User and Session interface
export interface UserWithSession {
user: {
id: number;
name: string;
username: string;
email: string;
role: UserRole;
status: UserStatus;
last_login: Date | null;
password_changed_at: Date;
created_at: Date;
updated_at: Date;
};
session: {
id: number;
session_token: string;
refresh_token: string;
ip_address: string;
user_agent: string | null;
expires_at: Date;
created_at: Date;
last_activity: Date;
};
}