From 2047e4bbf666bdff887fa1b2762aa08380496863 Mon Sep 17 00:00:00 2001 From: shankar Date: Sat, 7 Dec 2024 22:18:28 +0530 Subject: [PATCH] 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. --- db-scripts/01-init.sql | 108 +++++---- router-dashboard/src/App.tsx | 18 +- router-dashboard/src/components/Login.tsx | 73 ++++++ ve-router-backend/.env | 2 +- ve-router-backend/package.json | 8 +- ve-router-backend/src/app.ts | 15 ++ .../src/controllers/AuthController.ts | 183 +++++++++++++++ .../src/controllers/DicomStudyController.ts | 42 +--- .../src/controllers/RouterController.ts | 2 +- .../src/controllers/SetupController.ts | 23 ++ .../src/controllers/UserController.ts | 56 +++++ ve-router-backend/src/controllers/index.ts | 1 + ve-router-backend/src/middleware/auth.ts | 71 ++++++ ve-router-backend/src/middleware/index.ts | 1 + .../src/repositories/UserRepository.ts | 217 ++++++++++++++++++ ve-router-backend/src/routes/auth.routes.ts | 21 ++ ve-router-backend/src/routes/dicom.routes.ts | 4 + ve-router-backend/src/routes/index.ts | 6 + ve-router-backend/src/routes/router.routes.ts | 4 + ve-router-backend/src/routes/setup.routes.ts | 10 + ve-router-backend/src/routes/user.routes.ts | 19 ++ ve-router-backend/src/services/AuthService.ts | 122 ++++++++++ .../src/services/CommonService.ts | 25 ++ .../src/services/SetupService.ts | 45 ++++ ve-router-backend/src/services/UserService.ts | 50 ++++ .../src/services/UtilityService.ts | 2 +- ve-router-backend/src/services/index.ts | 4 + ve-router-backend/src/types/error.ts | 6 + ve-router-backend/src/types/user.ts | 86 +++++++ 29 files changed, 1141 insertions(+), 83 deletions(-) create mode 100644 router-dashboard/src/components/Login.tsx create mode 100644 ve-router-backend/src/controllers/AuthController.ts create mode 100644 ve-router-backend/src/controllers/SetupController.ts create mode 100644 ve-router-backend/src/controllers/UserController.ts create mode 100644 ve-router-backend/src/middleware/auth.ts create mode 100644 ve-router-backend/src/repositories/UserRepository.ts create mode 100644 ve-router-backend/src/routes/auth.routes.ts create mode 100644 ve-router-backend/src/routes/setup.routes.ts create mode 100644 ve-router-backend/src/routes/user.routes.ts create mode 100644 ve-router-backend/src/services/AuthService.ts create mode 100644 ve-router-backend/src/services/CommonService.ts create mode 100644 ve-router-backend/src/services/SetupService.ts create mode 100644 ve-router-backend/src/services/UserService.ts create mode 100644 ve-router-backend/src/types/error.ts create mode 100644 ve-router-backend/src/types/user.ts diff --git a/db-scripts/01-init.sql b/db-scripts/01-init.sql index 7fdc1f2..5b3be98 100644 --- a/db-scripts/01-init.sql +++ b/db-scripts/01-init.sql @@ -23,50 +23,6 @@ CREATE TABLE IF NOT EXISTS routers ( 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 CREATE TABLE IF NOT EXISTS container_status ( 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 ); +-- 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... diff --git a/router-dashboard/src/App.tsx b/router-dashboard/src/App.tsx index 3e0dbba..aab7bd7 100644 --- a/router-dashboard/src/App.tsx +++ b/router-dashboard/src/App.tsx @@ -1,7 +1,21 @@ +import React, { useState, useEffect } from 'react'; +import Login from './components/Login'; import DashboardLayout from './components/dashboard/DashboardLayout'; function App() { - return ; + const [isLoggedIn, setIsLoggedIn] = useState(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 ? : } + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/router-dashboard/src/components/Login.tsx b/router-dashboard/src/components/Login.tsx new file mode 100644 index 0000000..fb72f82 --- /dev/null +++ b/router-dashboard/src/components/Login.tsx @@ -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 ( +
+ {/* Header */} +
{}} /> + + {/* Login Content */} +
+
+

Login

+
+
+ + 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" + /> +
+
+ + 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" + /> +
+ {error && ( +
{error}
+ )} + +
+
+
+
+ ); +}; + +export default Login; diff --git a/ve-router-backend/.env b/ve-router-backend/.env index 382874f..0e62650 100644 --- a/ve-router-backend/.env +++ b/ve-router-backend/.env @@ -12,7 +12,7 @@ DB_NAME=ve_router_db DB_CONNECTION_LIMIT=10 # Authentication Configuration -JWT_SECRET=your-super-secure-jwt-secret-key +JWT_SECRET=VE_Router_JWT_Secret_2024@Key JWT_EXPIRES_IN=1d SALT_ROUNDS=10 diff --git a/ve-router-backend/package.json b/ve-router-backend/package.json index 9c6b815..8417afa 100644 --- a/ve-router-backend/package.json +++ b/ve-router-backend/package.json @@ -17,7 +17,9 @@ "express": "^4.18.2", "mysql2": "^3.2.0", "ve-router-backend": "file:", - "winston": "^3.16.0" + "winston": "^3.16.0", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2" }, "devDependencies": { "@types/cors": "^2.8.13", @@ -28,6 +30,8 @@ "eslint": "^8.37.0", "nodemon": "^2.0.22", "ts-node": "^10.9.2", - "typescript": "^5.0.3" + "typescript": "^5.0.3", + "@types/bcryptjs": "^2.4.3", + "@types/jsonwebtoken": "^9.0.2" } } diff --git a/ve-router-backend/src/app.ts b/ve-router-backend/src/app.ts index c34e17a..1a77b3c 100644 --- a/ve-router-backend/src/app.ts +++ b/ve-router-backend/src/app.ts @@ -5,6 +5,8 @@ import config from './config/config'; import routes from './routes'; import { errorHandler } from './middleware'; import logger from './utils/logger'; +import pool from './config/db'; +import { SetupService } from './services'; 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; app.listen(port, () => { logger.info(`Server running on port ${port} in ${config.env} mode`); diff --git a/ve-router-backend/src/controllers/AuthController.ts b/ve-router-backend/src/controllers/AuthController.ts new file mode 100644 index 0000000..70a4590 --- /dev/null +++ b/ve-router-backend/src/controllers/AuthController.ts @@ -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 = { + 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 => { + 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' }); + } + }; + +} diff --git a/ve-router-backend/src/controllers/DicomStudyController.ts b/ve-router-backend/src/controllers/DicomStudyController.ts index 7eada7a..38606f3 100644 --- a/ve-router-backend/src/controllers/DicomStudyController.ts +++ b/ve-router-backend/src/controllers/DicomStudyController.ts @@ -1,39 +1,17 @@ // src/controllers/DicomStudyController.ts import { Request, Response, NextFunction } from 'express'; -import { DicomStudyService } from '../services/DicomStudyService'; +import { DicomStudyService, CommonService} from '../services'; import logger from '../utils/logger'; import { Pool } from 'mysql2/promise'; -interface ApiError { - message: string; - code?: string; - stack?: string; -} - export class DicomStudyController { private service: DicomStudyService; + private commonService: CommonService; constructor(pool:Pool) { this.service = new DicomStudyService(pool); - } - - 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; + this.commonService = new CommonService(); } getAllStudies = async (req: Request, res: Response, next: NextFunction) => { @@ -41,7 +19,7 @@ export class DicomStudyController { const studies = await this.service.getAllStudies(); res.json(studies); } 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 }); } }; @@ -60,7 +38,7 @@ export class DicomStudyController { res.json(study); } 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 }); } }; @@ -75,7 +53,7 @@ export class DicomStudyController { const studies = await this.service.getStudiesByRouterId(routerId); res.json(studies); } 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 (error instanceof Error && error.message.includes('Invalid router_id')) { return res.status(404).json({ error: 'Router not found' }); @@ -118,7 +96,7 @@ export class DicomStudyController { const study = await this.service.createStudy(req.body); res.status(201).json(study); } 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 if (error instanceof Error) { @@ -154,7 +132,7 @@ export class DicomStudyController { res.json(study); } 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 }); } }; @@ -173,7 +151,7 @@ export class DicomStudyController { res.status(204).send(); } 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 }); } }; @@ -204,7 +182,7 @@ export class DicomStudyController { res.json(studies); } 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 }); } }; diff --git a/ve-router-backend/src/controllers/RouterController.ts b/ve-router-backend/src/controllers/RouterController.ts index 63ffc25..bfe618e 100644 --- a/ve-router-backend/src/controllers/RouterController.ts +++ b/ve-router-backend/src/controllers/RouterController.ts @@ -10,7 +10,7 @@ import logger from '../utils/logger'; export class RouterController { private service: RouterService; private dicomStudyService: DicomStudyService; - private utilityService: UtilityService + private utilityService: UtilityService; constructor(pool: Pool) { this.service = new RouterService(pool); diff --git a/ve-router-backend/src/controllers/SetupController.ts b/ve-router-backend/src/controllers/SetupController.ts new file mode 100644 index 0000000..59e4411 --- /dev/null +++ b/ve-router-backend/src/controllers/SetupController.ts @@ -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 }); + } + }; + +}; diff --git a/ve-router-backend/src/controllers/UserController.ts b/ve-router-backend/src/controllers/UserController.ts new file mode 100644 index 0000000..dd57705 --- /dev/null +++ b/ve-router-backend/src/controllers/UserController.ts @@ -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) => { + + }; + +} diff --git a/ve-router-backend/src/controllers/index.ts b/ve-router-backend/src/controllers/index.ts index 546536e..de17c3c 100644 --- a/ve-router-backend/src/controllers/index.ts +++ b/ve-router-backend/src/controllers/index.ts @@ -1,3 +1,4 @@ export * from './RouterController'; export * from './DicomStudyController'; +export * from './AuthController'; // Add more controller exports as needed diff --git a/ve-router-backend/src/middleware/auth.ts b/ve-router-backend/src/middleware/auth.ts new file mode 100644 index 0000000..ea78767 --- /dev/null +++ b/ve-router-backend/src/middleware/auth.ts @@ -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' }); + } +}; diff --git a/ve-router-backend/src/middleware/index.ts b/ve-router-backend/src/middleware/index.ts index 6adc831..e78f0e2 100644 --- a/ve-router-backend/src/middleware/index.ts +++ b/ve-router-backend/src/middleware/index.ts @@ -1,2 +1,3 @@ export * from './errorHandler'; +export * from './auth'; // Add more middleware exports as needed diff --git a/ve-router-backend/src/repositories/UserRepository.ts b/ve-router-backend/src/repositories/UserRepository.ts new file mode 100644 index 0000000..6c3c761 --- /dev/null +++ b/ve-router-backend/src/repositories/UserRepository.ts @@ -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 { + const [rows] = await pool.query( + 'SELECT * FROM users WHERE id = ?', + [id] + ); + + if (!rows.length) return null; + return rows[0] as User; + } + + async findByUsername(username: string): Promise { + const [rows] = await pool.query( + '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 { + const [rows] = await pool.query( + '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): Promise { + const [result] = await pool.query( + `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; + } + + async update(id: number, userData: UpdateUser): Promise { + 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 { + const [rows] = await pool.query( + 'SELECT * FROM user_sessions WHERE id = ?', + [id] + ); + + if (!rows.length) return null; + return rows[0] as UserSession; + } + + async getUserSessionByIp(userId: number, ipAdress: string): Promise { + const [rows] = await pool.query( + `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 { + const [rows] = await pool.query( + `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 { + const [rows] = await pool.query( + `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): Promise { + 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): Promise { + 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(` + 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 { + const [result] = await pool.query( + 'DELETE FROM user_sessions WHERE refresh_token = ?', + [refreshToken] + ); + return result.affectedRows > 0; + } + +} \ No newline at end of file diff --git a/ve-router-backend/src/routes/auth.routes.ts b/ve-router-backend/src/routes/auth.routes.ts new file mode 100644 index 0000000..fbf1031 --- /dev/null +++ b/ve-router-backend/src/routes/auth.routes.ts @@ -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; \ No newline at end of file diff --git a/ve-router-backend/src/routes/dicom.routes.ts b/ve-router-backend/src/routes/dicom.routes.ts index a2bb312..35b09b7 100644 --- a/ve-router-backend/src/routes/dicom.routes.ts +++ b/ve-router-backend/src/routes/dicom.routes.ts @@ -4,6 +4,7 @@ import express from 'express'; import { DicomStudyController } from '../controllers/DicomStudyController'; import logger from '../utils/logger'; import pool from '../config/db'; // If using default export +import { authMiddleware } from '../middleware/auth'; const router = express.Router(); const dicomStudyController = new DicomStudyController(pool); @@ -22,6 +23,9 @@ router.get('/test', (req, res) => { res.json({ message: 'DICOM routes are working' }); }); +// Protected routes +router.use(authMiddleware(pool)); + router.post('/', (req, res, next) => { logger.debug('POST / route hit with body:', req.body); dicomStudyController.createStudy(req, res, next); diff --git a/ve-router-backend/src/routes/index.ts b/ve-router-backend/src/routes/index.ts index a19d0d3..944d87d 100644 --- a/ve-router-backend/src/routes/index.ts +++ b/ve-router-backend/src/routes/index.ts @@ -3,6 +3,8 @@ import { Router } from 'express'; import routerRoutes from './router.routes'; import dicomRoutes from './dicom.routes'; import logger from '../utils/logger'; +import authRoutes from './auth.routes'; +import userRoutes from './user.routes'; const router = Router(); @@ -10,9 +12,13 @@ const router = Router(); logger.info('Registering routes:'); logger.info('- /routers -> router routes'); logger.info('- /studies -> dicom routes'); +logger.info('- /auth -> auth routes'); +logger.info('- /users -> user routes'); router.use('/routers', routerRoutes); router.use('/studies', dicomRoutes); +router.use('/auth', authRoutes); +router.use('/users', userRoutes); // Debug middleware to log incoming requests router.use((req, res, next) => { diff --git a/ve-router-backend/src/routes/router.routes.ts b/ve-router-backend/src/routes/router.routes.ts index e3c7197..839bc36 100644 --- a/ve-router-backend/src/routes/router.routes.ts +++ b/ve-router-backend/src/routes/router.routes.ts @@ -2,6 +2,7 @@ import { Router } from 'express'; import pool from '../config/db'; // If using default export import { RouterController } from '../controllers/RouterController'; +import { authMiddleware } from '../middleware/auth'; @@ -9,6 +10,9 @@ import { RouterController } from '../controllers/RouterController'; const router = Router(); const controller = new RouterController(pool); +// Protected routes +router.use(authMiddleware(pool)); + router.put('/vms', async (req, res, next) => { console.log('Route handler: /vms endpoint hit'); console.log('Query params:', req.query); diff --git a/ve-router-backend/src/routes/setup.routes.ts b/ve-router-backend/src/routes/setup.routes.ts new file mode 100644 index 0000000..d792d46 --- /dev/null +++ b/ve-router-backend/src/routes/setup.routes.ts @@ -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; diff --git a/ve-router-backend/src/routes/user.routes.ts b/ve-router-backend/src/routes/user.routes.ts new file mode 100644 index 0000000..20d2e30 --- /dev/null +++ b/ve-router-backend/src/routes/user.routes.ts @@ -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; \ No newline at end of file diff --git a/ve-router-backend/src/services/AuthService.ts b/ve-router-backend/src/services/AuthService.ts new file mode 100644 index 0000000..fa2c3d5 --- /dev/null +++ b/ve-router-backend/src/services/AuthService.ts @@ -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) { + 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) { + 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 { + 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 { + return await this.userService.getUserById(id); + }; + + async createUserSession (userSessionData: Partial) { + 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 { + return await this.userService.getUserSessionByIp(userId, ipAdress); + }; + async getUserSessionByUserAndAgent (userId: number, userAgent: string): Promise { + return await this.userService.getUserSessionByUserAndAgent(userId, userAgent); + }; + + async getUserAndSessionByRefreshToken (refreshToken: string): Promise { + return this.userService.getUserAndSessionByRefreshToken(refreshToken); + }; + + async deleteUserSession (refreshToken: string) { + this.userService.deleteUserSession(refreshToken); + }; + + async updateUserSession (refreshToken:string, userSessionData: Partial) { + 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); + }; + +} diff --git a/ve-router-backend/src/services/CommonService.ts b/ve-router-backend/src/services/CommonService.ts new file mode 100644 index 0000000..d6a98a8 --- /dev/null +++ b/ve-router-backend/src/services/CommonService.ts @@ -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; + } + +} + \ No newline at end of file diff --git a/ve-router-backend/src/services/SetupService.ts b/ve-router-backend/src/services/SetupService.ts new file mode 100644 index 0000000..9fd1b09 --- /dev/null +++ b/ve-router-backend/src/services/SetupService.ts @@ -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; + }; + +}; \ No newline at end of file diff --git a/ve-router-backend/src/services/UserService.ts b/ve-router-backend/src/services/UserService.ts new file mode 100644 index 0000000..fbd670f --- /dev/null +++ b/ve-router-backend/src/services/UserService.ts @@ -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 { + return this.repository.findByUsername(username); + } + + async updateUser(id: number, user: UpdateUser): Promise { + return this.repository.update(id, user); + } + + async getUserById(id: number): Promise { + return await this.repository.findById(id); + } + + async getUserSessionByIp (userId: number, ipAdress: string): Promise { + return await this.repository.getUserSessionByIp(userId, ipAdress); + } + + async getUserSessionByUserAndAgent (userId: number, userAgent: string): Promise { + return await this.repository.getUserSessionByUserAndAgent(userId, userAgent); + } + + async createUserSession(userSessionDTO: Partial): Promise { + return this.repository.createUserSession(userSessionDTO); + } + + async getUserAndSessionByRefreshToken (refreshToken: string): Promise { + return this.repository.getUserAndSessionByRefreshToken(refreshToken); + }; + + async updateUserSession (refreshToken:string, userSessionData: Partial) { + return this.repository.updateUserSession(refreshToken, userSessionData); + } + + async deleteUserSession(refreshToken: string): Promise { + return this.repository.deleteUserSession(refreshToken); + } + +} \ No newline at end of file diff --git a/ve-router-backend/src/services/UtilityService.ts b/ve-router-backend/src/services/UtilityService.ts index c20ca91..d5d399c 100644 --- a/ve-router-backend/src/services/UtilityService.ts +++ b/ve-router-backend/src/services/UtilityService.ts @@ -11,7 +11,7 @@ export class UtilityService { } else { return 'DISK_NORMAL'; // Normal disk status } - } + } } \ No newline at end of file diff --git a/ve-router-backend/src/services/index.ts b/ve-router-backend/src/services/index.ts index 1a7c373..e1ea81c 100644 --- a/ve-router-backend/src/services/index.ts +++ b/ve-router-backend/src/services/index.ts @@ -1,4 +1,8 @@ export * from './RouterService'; export * from './DicomStudyService'; export * from './UtilityService'; +export * from './AuthService'; +export * from './UserService'; +export * from './CommonService'; +export * from './SetupService'; // Add more service exports as needed diff --git a/ve-router-backend/src/types/error.ts b/ve-router-backend/src/types/error.ts new file mode 100644 index 0000000..33d15a7 --- /dev/null +++ b/ve-router-backend/src/types/error.ts @@ -0,0 +1,6 @@ + +export interface ApiError { + message: string; + code?: string; + stack?: string; +} \ No newline at end of file diff --git a/ve-router-backend/src/types/user.ts b/ve-router-backend/src/types/user.ts new file mode 100644 index 0000000..55c48b2 --- /dev/null +++ b/ve-router-backend/src/types/user.ts @@ -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; + }; +}