Compare commits

..

7 Commits

Author SHA1 Message Date
7c288da183 Redirect to login page when no activity for 30 minutes. Show only facility AET and VM IP. Add default users for now because user management is not yet implemented. 2024-12-11 18:10:17 +05:30
262307f17d Added code to validate if user authenticated then redirect to dashboard else login page. Added code to handle role based navebar tab displaying. Added code to handle access token or refresh token expiration and then redirect to login page. 2024-12-09 17:06:01 +05:30
ea7ac4cc72 Included name to return in response 2024-12-07 22:40:05 +05:30
2047e4bbf6 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. 2024-12-07 22:18:28 +05:30
dfa974fe6b Remove study count showing on Activity column. Collapse Navigation bar by default. Show Active and Idle for router activity. Fixed card view count. Fixed card view router row info as per correct status. Show only one recent study on Activity column. Show router details on click of router id to expanded row. 2024-11-28 20:08:10 +05:30
8630a3e2c5 Fixed React backend startup issue.
Updated SQL script to correct procedure and column sizes.
Moved SQL scripts to the project root folder (outside React frontend).
Resolved backend container dependency issue to ensure MySQL is up before starting React backend.
Moved common values to .env file in the project root.
Updated React backend and MySQL ports to use default values.
Added code to get last study received, containers status and updated into DB.
2024-11-22 13:44:03 +05:30
8fe130f918 Fixed React backend startup issue.
Updated SQL script to correct procedure and column sizes.
Moved SQL scripts to the project root folder (outside React frontend).
Resolved backend container dependency issue to ensure MySQL is up before starting React backend.
Moved common values to .env file in the project root.
Updated React backend and MySQL ports to use default values.
2024-11-18 10:16:05 +05:30
67 changed files with 2713 additions and 734 deletions

7
.env Normal file
View File

@ -0,0 +1,7 @@
VITE_API_URL=http://localhost:3000/api/v1
NODE_ENV=development
#NODE_ENV=production
#Database Configuration
DB_NAME=ve_router_db

25
db-scripts/00-init-db.sh Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# Wait for MySQL to become available
echo "Waiting for MySQL to become healthy..."
until mysql -h "localhost" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SHOW DATABASES;" &>/dev/null; do
echo "Waiting for MySQL..."
sleep 2
done
echo "MySQL is up and running!"
# Run the initial common setup (always executed)
echo "Running 01-init.sql"
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < /docker-entrypoint-initdb.d/01-init.sql
echo "Running 02-seed_common_data.sql"
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < /docker-entrypoint-initdb.d/02-seed_common_data.sql
# Check the environment variable and execute environment-specific scripts
if [ "$ENVIRONMENT" != "production" ]; then
echo "Running 03-seed_router_data_qa.sql for QA (non-production)"
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < /docker-entrypoint-initdb.d/03-seed_router_data_qa.sql
else
echo "Production environment detected. Skipping seeding data for QA (non-production)."
fi

View File

@ -8,9 +8,13 @@ CREATE TABLE IF NOT EXISTS routers (
router_id VARCHAR(10) UNIQUE NOT NULL, -- Unique router identifier
facility VARCHAR(50) NOT NULL,
router_alias VARCHAR(50) NOT NULL,
facility_aet VARCHAR(50) NOT NULL,
openvpn_ip VARCHAR(15) NOT NULL,
router_vm_primary_ip VARCHAR(15) NOT NULL,
last_seen TIMESTAMP NOT NULL,
vpn_status_code VARCHAR(50) NOT NULL,
disk_status_code VARCHAR(50) NOT NULL,
app_status_code VARCHAR(50) NOT NULL,
license_status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'inactive',
free_disk BIGINT NOT NULL CHECK (free_disk >= 0),
total_disk BIGINT NOT NULL CHECK (total_disk > 0),
@ -19,98 +23,36 @@ 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)
);
-- System status table
CREATE TABLE IF NOT EXISTS system_status (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Container status table
CREATE TABLE IF NOT EXISTS container_status (
id INT AUTO_INCREMENT PRIMARY KEY,
system_status_id INT NOT NULL,
container_number INT NOT NULL CHECK (container_number BETWEEN 1 AND 10),
status_code VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- VM details table
CREATE TABLE IF NOT EXISTS vm_details (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id INT NOT NULL,
vm_number INT NOT NULL CHECK (vm_number > 0),
status_code VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT unique_vm_per_router UNIQUE(router_id, vm_number)
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
router_id varchar(50) NOT NULL,
container_name varchar(50) NOT NULL,
status_code varchar(50) NOT NULL,
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE(router_id, container_name)
);
-- DICOM study overview table with router_id as a string reference
CREATE TABLE IF NOT EXISTS dicom_study_overview (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id VARCHAR(10) NOT NULL, -- Matching VARCHAR(10) with the routers table
study_instance_uid VARCHAR(100) UNIQUE NOT NULL,
patient_id VARCHAR(50) NOT NULL,
patient_name VARCHAR(100) NOT NULL,
accession_number VARCHAR(50) NOT NULL,
study_date DATE NOT NULL,
modality VARCHAR(20) NOT NULL,
study_description VARCHAR(255),
series_instance_uid VARCHAR(100) NOT NULL,
procedure_code VARCHAR(50),
referring_physician_name VARCHAR(100),
study_status_code VARCHAR(50) NOT NULL DEFAULT 'NEW', -- Default value, ensure 'NEW' exists in status_type
association_id VARCHAR(50) NOT NULL DEFAULT 'NEW',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS dicom_study_overview (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id VARCHAR(10) NOT NULL, -- Matching VARCHAR(10) with the routers table
study_instance_uid VARCHAR(100) UNIQUE NOT NULL,
patient_id VARCHAR(50) NOT NULL,
patient_name VARCHAR(100) NOT NULL,
accession_number VARCHAR(50) NOT NULL,
study_date DATE NOT NULL,
modality VARCHAR(20) NOT NULL,
study_description VARCHAR(255),
series_instance_uid VARCHAR(100) NOT NULL,
procedure_code VARCHAR(50),
referring_physician_name VARCHAR(100),
study_status_code VARCHAR(50) NOT NULL DEFAULT 'NEW', -- Default value, ensure 'NEW' exists in status_type
association_id VARCHAR(50) NOT NULL DEFAULT 'NEW',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Create tables if they don't exist
CREATE TABLE IF NOT EXISTS status_type (
@ -118,7 +60,7 @@ CREATE TABLE IF NOT EXISTS status_type (
category_id VARCHAR(50),
name VARCHAR(100),
code VARCHAR(100),
description VARCHAR(20),
description VARCHAR(150),
severity INT
);
@ -141,5 +83,67 @@ 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,
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_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...

View File

@ -0,0 +1,37 @@
-- Check if the procedure exists, and create it only if it does not
DROP PROCEDURE IF EXISTS `seed_common_router_data`;
DELIMITER //
CREATE PROCEDURE seed_common_router_data()
BEGIN
-- Insert Status Categories
INSERT INTO status_category (name, description)
VALUES
('Network', 'Network related statuses'),
('Disk', 'Disk related statuses'),
('VPN', 'VPN connection statuses'),
('License', 'License statuses'),
('Container', 'Container related statuses')
ON DUPLICATE KEY UPDATE id = id;
-- Insert Status Types
INSERT INTO status_type (category_id, name, code, description, severity)
VALUES
(1, 'Online', 'NET_ONLINE', 'System is online', 1),
(1, 'Offline', 'NET_OFFLINE', 'System is offline', 5),
(2, 'Normal', 'DISK_NORMAL', 'Disk usage is normal', 1),
(2, 'Warning', 'DISK_WARNING', 'Disk usage is high', 3),
(2, 'Critical', 'DISK_CRITICAL', 'Disk usage is critical', 5),
(3, 'Connected', 'VPN_CONNECTED', 'VPN is connected', 1),
(3, 'Disconnected', 'VPN_DISCONNECTED', 'VPN is disconnected', 5),
(5, 'Running', 'CONTAINER_RUNNING', 'Container is running', 1),
(5, 'Stopped', 'CONTAINER_STOPPED', 'Container is stopped', 5)
ON DUPLICATE KEY UPDATE id = id;
END //
DELIMITER ;
-- Automatically call the procedure after creation
CALL seed_common_router_data();

View File

@ -0,0 +1,49 @@
-- Check if the procedure exists, and create it only if it does not
DROP PROCEDURE IF EXISTS `seed_router_data`;
DELIMITER //
CREATE PROCEDURE seed_router_data()
BEGIN
-- Insert Routers
INSERT INTO routers (router_id, facility, router_alias, facility_aet, openvpn_ip, router_vm_primary_ip,
last_seen, vpn_status_code, disk_status_code, app_status_code, license_status, free_disk, total_disk, disk_usage)
VALUES
('RTR001', 'Main Hospital', 'MAIN_RAD', 'RTR_1', '10.8.0.101', '192.168.1.101', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'CONTAINER_RUNNING', 'active', 500000000000, 1000000000000, 50.00),
('RTR002', 'Emergency Center', 'ER_RAD', 'RTR_2', '10.8.0.102', '192.168.1.102', NOW(), 'VPN_DISCONNECTED', 'DISK_WARNING', 'CONTAINER_RUNNING', 'active', 400000000000, 1000000000000, 60.00),
('RTR003', 'Imaging Center', 'IMG_CENTER', 'RTR_3', '10.8.0.103', '192.168.1.103', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'CONTAINER_RUNNING', 'active', 600000000000, 1000000000000, 40.00)
ON DUPLICATE KEY UPDATE id = id;
-- Store Router IDs
SET @router1_id = (SELECT id FROM routers WHERE router_id = 'RTR001');
SET @router2_id = (SELECT id FROM routers WHERE router_id = 'RTR002');
SET @router3_id = (SELECT id FROM routers WHERE router_id = 'RTR003');
-- Insert Container Status
INSERT INTO container_status (router_id, container_name, status_code, created_at, updated_at)
VALUES
(@router1_id, 'router-cstore-scp', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router1_id, 'router-cstore-scu', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router2_id, 'router-cstore-scp', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router2_id, 'router-cstore-scu', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router3_id, 'router-cstore-scp', 'CONTAINER_RUNNING', NOW(), NOW())
ON DUPLICATE KEY UPDATE id = id;
-- Insert DICOM Study Overview
INSERT INTO dicom_study_overview (
router_id, study_instance_uid, patient_id, patient_name,
accession_number, study_date, modality, study_description,
series_instance_uid, procedure_code, referring_physician_name,
study_status_code
)
VALUES
(@router1_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.134', 'P1', 'John Doe', 'ACC1234', '2024-03-15', 'CT', 'Chest CT', '1.2.840.113619.2.55.3.283116435.276.1543707219.135', 'CT001', 'Dr. Smith', 'idle'),
(@router2_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.136', 'P2', 'Jane Doe', 'ACC1235', '2024-03-15', 'MR', 'Brain MRI', '1.2.840.113619.2.55.3.283116435.276.1543707219.137', 'MR001', 'Dr. Johnson', 'idle')
ON DUPLICATE KEY UPDATE id = id;
END //
DELIMITER ;
-- Automatically call the procedure after creation
CALL seed_router_data();

142
deploy.sh Normal file
View File

@ -0,0 +1,142 @@
#!/bin/bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored messages
print_message() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Function to check if command was successful
check_status() {
if [ $? -eq 0 ]; then
print_message "$GREEN" "✔ Success: $1"
else
print_message "$RED" "✘ Error: $1"
exit 1
fi
}
# Default values
ENV="production"
SERVER_IP=""
FRONTEND_IP=""
DB_HOST="mysql"
# Help message
show_help() {
echo "Usage: ./deploy.sh [OPTIONS]"
echo "Deploy the router dashboard application"
echo
echo "Options:"
echo " -s, --server-ip Server IP address (required)"
echo " -f, --frontend-ip Frontend IP address (defaults to server IP if not provided)"
echo " -d, --db-host Database host (defaults to mysql)"
echo " -e, --environment Environment (development/staging/production, defaults to production)"
echo " -h, --help Show this help message"
echo
echo "Example:"
echo " ./deploy.sh -s 192.168.1.100 -f 192.168.1.101 -e production"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-s|--server-ip)
SERVER_IP="$2"
shift 2
;;
-f|--frontend-ip)
FRONTEND_IP="$2"
shift 2
;;
-d|--db-host)
DB_HOST="$2"
shift 2
;;
-e|--environment)
ENV="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
print_message "$RED" "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Validate required parameters
if [ -z "$SERVER_IP" ]; then
print_message "$RED" "Error: Server IP is required"
show_help
exit 1
fi
# If frontend IP is not provided, use server IP
if [ -z "$FRONTEND_IP" ]; then
FRONTEND_IP=$SERVER_IP
print_message "$YELLOW" "Frontend IP not provided, using Server IP: $FRONTEND_IP"
fi
# Export environment variables
export SERVER_IP
export FRONTEND_IP
export DB_HOST
export NODE_ENV=$ENV
# Display deployment information
print_message "$GREEN" "\nDeployment Configuration:"
echo "Server IP: $SERVER_IP"
echo "Frontend IP: $FRONTEND_IP"
echo "Database Host: $DB_HOST"
echo "Environment: $ENV"
echo
# Confirm deployment
read -p "Do you want to continue with deployment? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_message "$YELLOW" "Deployment cancelled"
exit 1
fi
# Start deployment
print_message "$GREEN" "\nStarting deployment..."
# Pull latest changes
print_message "$YELLOW" "Pulling latest changes..."
git pull
check_status "Git pull"
# Stop existing containers
print_message "$YELLOW" "Stopping existing containers..."
docker-compose down
check_status "Stopping containers"
# Build containers
print_message "$YELLOW" "Building containers..."
docker-compose build
check_status "Building containers"
# Start containers
print_message "$YELLOW" "Starting containers..."
docker-compose up -d
check_status "Starting containers"
# Check container status
print_message "$YELLOW" "Checking container status..."
docker-compose ps
check_status "Container status check"
print_message "$GREEN" "\nDeployment completed successfully!"

View File

@ -4,57 +4,87 @@ services:
frontend:
build:
context: ./router-dashboard
dockerfile: Dockerfile
dockerfile: dockerfile
args:
- SERVER_IP=${SERVER_IP:-localhost}
ports:
- "5173:5173"
- "${FRONTEND_PORT:-5173}:5173"
environment:
- VITE_API_URL=http://localhost:3001/api/v1
- SERVER_IP=${SERVER_IP:-localhost}
- FRONTEND_IP=${FRONTEND_IP:-localhost}
- VITE_API_URL=http://${SERVER_IP:-localhost}:${BACKEND_PORT:-3000}/api/v1
- VITE_NODE_ENV=${NODE_ENV:-development}
restart: always
depends_on:
- backend
volumes:
- ./router-dashboard:/app
- /app/node_modules
backend:
condition: service_healthy
container_name: router_dashboard_frontend
networks:
- app_network
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://${SERVER_IP:-localhost}:5173"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
backend:
build:
context: ./ve-router-backend
dockerfile: Dockerfile
dockerfile: dockerfile
ports:
- "3001:3000"
- "${BACKEND_PORT:-3000}:3000"
environment:
- NODE_ENV=development
- DB_HOST=host.docker.internal
- DB_PORT=3307
- DB_USER=ve_router_user
- DB_PASSWORD=ve_router_password
- DB_NAME=ve_router_db
- NODE_ENV=${NODE_ENV:-development}
- DB_HOST=${DB_HOST:-mysql}
- DB_PORT=${DB_PORT:-3306}
- DB_USER=${DB_USER:-ve_router_user}
- DB_PASSWORD=${DB_PASSWORD:-ve_router_password}
- DB_NAME=${DB_NAME:-ve_router_db}
- CORS_ORIGIN=http://${FRONTEND_IP:-localhost}:${FRONTEND_PORT:-5173},http://${SERVER_IP:-localhost}:${BACKEND_PORT:-3000}
restart: always
depends_on:
- mysql
volumes:
- ./ve-router-backend:/app
- /app/node_modules
mysql:
condition: service_healthy
healthcheck:
test: ["CMD", "nc", "-z", "${SERVER_IP:-localhost}", "${BACKEND_PORT:-3000}"]
interval: 30s
retries: 3
start_period: 30s
timeout: 10s
networks:
- app_network
container_name: router_dashboard_backend
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: ve_router_db
MYSQL_USER: ve_router_user
MYSQL_PASSWORD: ve_router_password
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: ${DB_NAME:-ve_router_db}
MYSQL_USER: ${DB_USER:-ve_router_user}
MYSQL_PASSWORD: ${DB_PASSWORD:-ve_router_password}
ENVIRONMENT: ${NODE_ENV:-development}
volumes:
- mysql_data:/var/lib/mysql
# Correct paths for init scripts
- ./router-dashboard/sql/init.sql:/docker-entrypoint-initdb.d/01-init.sql
- ./router-dashboard/sql/seed_data.sql:/docker-entrypoint-initdb.d/02-seed_data.sql
- ./db-scripts:/docker-entrypoint-initdb.d:ro
ports:
- "3307:3306"
- "${DB_PORT:-3306}:3306"
command: --default-authentication-plugin=mysql_native_password
restart: always
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ve_router_user", "-pve_router_password"]
test: ["CMD", "mysqladmin", "ping", "-h", "${DB_HOST:-mysql}", "-u${DB_USER:-ve_router_user}", "-p${DB_PASSWORD:-ve_router_password}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- app_network
container_name: router_dashboard_mysql
networks:
app_network:
driver: bridge
volumes:
mysql_data:
name: router_dashboard_mysql_data

38
readme.txt Normal file
View File

@ -0,0 +1,38 @@
1. Go to router-dashboard directory
2. Run below command to build the docker images, create sql schema, insert data and start containers
docker-compose up --build -d
3. When code changes done, then just run above command mentioned in point 2,
it will udpate the changes and restart containers for which code changed
4. Open below URL in web browser to verify UI
http://localhost:5173
5. Open mysql workbench/any tool to view schema details
database:ve_router_db
host: localhost
port:3306
user/password: ve_router_user/ve_router_password
6. Run below command to stop and remove containers
docker-compose down
7. Run below command to stop, remove and delete all the volumes
Caution : if mysql has volumes then all the existing data will be erasesd
docker-compose down -v
Deploy Instructions:
1. Basic usage with just server IP:
./deploy.sh -s 192.168.1.100
2.Specify different IPs for frontend and backend:
./deploy.sh -s 192.168.1.100 -f 192.168.1.101
3.Specify environment:
./deploy.sh -s 192.168.1.100 -e staging
4.Full configuration:
./deploy.sh -s 192.168.1.100 -f 192.168.1.101 -d mysql -e production
5.Run it with the help flag to see options:
./deploy.sh --help
Important notes:
In production, you should use HTTPS instead of HTTP
Make sure your firewall rules allow the necessary ports (3000, 5173, 3306)
Consider using a reverse proxy like Nginx in front of your services
The MySQL container is accessible to other containers through the service name "mysql" when using Docker networks

View File

@ -1,2 +1,3 @@
VITE_API_URL=http://localhost:3001/api/v1
VITE_API_URL=http://${SERVER_IP:-localhost}:3000/api/v1
VITE_NODE_ENV=development
#VITE_NODE_ENV=production

View File

@ -0,0 +1,2 @@
VITE_API_URL=http://${SERVER_IP}:3000/api/v1
VITE_NODE_ENV=production

View File

@ -8,7 +8,7 @@ RUN npm install
COPY . .
ENV VITE_API_URL=http://localhost:3001/api/v1
ENV VITE_API_URL=http://localhost:3000/api/v1
EXPOSE 5173

View File

@ -2,9 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<link rel="icon" type="image/png" href="ve.png" >
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<!-- <title>Vite + React + TS </title> -->
<title>Router Management</title>
</head>
<body>
<div id="root"></div>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

View File

@ -1,7 +1,36 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import Login from './components/Login';
import DashboardLayout from './components/dashboard/DashboardLayout';
import { useAuth } from './contexts/AuthContext';
function App() {
return <DashboardLayout />;
const { isAuthenticated } = useAuth();
return (
<Routes>
<Route
path="/"
element={
isAuthenticated ? (
<DashboardLayout />
) : (
<Navigate to="/login" replace />
)
}
/>
<Route
path="/login"
element={
isAuthenticated ? (
<Navigate to="/" replace />
) : (
<Login />
)
}
/>
</Routes>
);
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1 +1,99 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 225.9 45.8" style="enable-background:new 0 0 225.9 45.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#231F52;}
.st1{fill:#05887A;}
.st2{fill:#87C35F;}
.st3{fill:#68C7D7;}
.st4{fill:#24767B;}
.st5{fill:#2377B5;}
.st6{fill:#069666;}
.st7{fill:#60B046;}
.st8{fill:#ECE843;}
.st9{fill:#D8DF25;}
.st10{fill:#204780;}
.st11{fill:#81BC42;}
.st12{fill:#244087;}
.st13{fill:#224480;}
.st14{fill:#148745;}
.st15{fill:#298DC9;}
.st16{fill:#17464A;}
.st17{fill:#2B67A8;}
.st18{fill:#25AC6D;}
.st19{fill:#276780;}
.st20{fill:#2783C1;}
.st21{fill:#2894CE;}
</style>
<g id="VitalEngine_Horizontal_4c" transform="translate(-135.213 -220.46)">
<g id="Group_3" transform="translate(188.083 239.605)">
<g id="Group_2" transform="translate(67.686)">
<g id="Group_1">
<path id="Path_1" class="st0" d="M14.4,22H0V0.9h14.4v2.9h-11v5.8h9.1v2.9H3.4v6.6h11V22z"/>
<path id="Path_2" class="st0" d="M18.2,22V5.7h3.1v2c2-1.7,2.7-2,4.1-2h2.6c1-0.1,2,0.2,2.8,0.9c0.6,0.6,1,1.4,1,3.7V22h-3.1
V10.5c0.1-0.6-0.1-1.1-0.4-1.6c-0.3-0.3-0.6-0.5-1.7-0.5h-2.1c-1.1,0-2.1,0.4-3,0.9V22L18.2,22L18.2,22z"/>
<path id="Path_3" class="st0" d="M39.8,26.5c-1,0.1-1.9-0.2-2.7-0.8c-0.6-0.6-0.9-1.4-0.9-3.2h3.1c0,0.7,0.1,1,0.3,1.2
c0.2,0.2,0.4,0.2,1.2,0.2h3.9c0.9,0,1.2-0.1,1.4-0.3c0.2-0.2,0.3-0.5,0.3-2v-3.4c-2.1,1.7-2.7,2-4.1,2h-2.2
c-1.7,0-2.6-0.3-3.3-1c-0.9-0.9-1.3-1.8-1.3-6.3s0.4-5.4,1.3-6.3c0.7-0.7,1.6-1,3.3-1h2.2c1.3,0,2,0.3,4.1,2v-2h3.1v15.5
c0,2.9-0.3,3.7-1,4.4c-0.5,0.5-1.3,0.9-3.2,0.9L39.8,26.5L39.8,26.5z M46.6,16.5V9.4c-1-0.6-2-0.9-3.2-1h-2.3
c-1,0-1.4,0.1-1.7,0.4C39,9.3,38.9,9.7,38.9,13s0.1,3.6,0.6,4.1c0.3,0.3,0.7,0.4,1.7,0.4h2.4C44.7,17.4,45.7,17.1,46.6,16.5
L46.6,16.5z"/>
<path id="Path_4" class="st0" d="M57.7,3.4h-3.6V0h3.6V3.4z M57.5,22h-3.1V5.7h3.1V22z"/>
<path id="Path_5" class="st0" d="M62.1,22V5.7h3.1v2c2-1.7,2.7-2,4.1-2h2.6c1-0.1,2,0.2,2.8,0.9c0.6,0.6,1,1.4,1,3.7V22h-3.1
V10.5c0.1-0.6-0.1-1.1-0.4-1.6c-0.3-0.3-0.6-0.5-1.7-0.5h-2.1c-1.1,0-2.1,0.4-3,0.9V22L62.1,22L62.1,22z"/>
<path id="Path_6" class="st0" d="M82.7,14.9c0,3.2,0.2,3.8,0.5,4.1s0.5,0.3,1.4,0.3h4.2c0.7,0,0.9-0.1,1.1-0.3
c0.2-0.2,0.3-0.6,0.3-2h3c-0.1,2.5-0.3,3.4-1.1,4.2c-0.7,0.6-1.7,0.9-2.7,0.8h-5.3c-1.2,0.1-2.3-0.2-3.2-0.9
c-1.1-1.1-1.4-2.3-1.4-7.2s0.3-6.1,1.4-7.2c0.7-0.7,1.6-0.9,3.2-0.9h4.5c1.2-0.1,2.3,0.2,3.2,0.9c1.1,1.1,1.4,2.3,1.4,7.1v0.8
c0,0.2-0.1,0.4-0.4,0.4L82.7,14.9L82.7,14.9z M82.7,12.4h7.4c0-2.7-0.2-3.3-0.5-3.6c-0.2-0.2-0.5-0.3-1.4-0.3h-3.7
c-0.9,0-1.2,0.1-1.4,0.3C82.9,9.1,82.8,9.6,82.7,12.4z"/>
</g>
</g>
<path id="Path_7" class="st0" d="M20.8,3.4h3.6V0h-3.6V3.4z M21,22h3.1V5.7H21L21,22z M9.4,18.7H9L3.6,0.9H0l6.4,20.4
c0.2,0.7,0.5,0.8,1.2,0.8h2.9c0.8,0,1-0.1,1.2-0.8l6.4-20.4h-3.4L9.4,18.7z M49.1,5.7H44c-1-0.1-2,0.2-2.7,0.9
c-0.6,0.6-0.9,1.4-1,4h3.1c0.1-1.2,0.1-1.8,0.4-2c0.2-0.2,0.5-0.3,1.3-0.3h3.2c0.9,0,1.2,0.1,1.4,0.3c0.2,0.2,0.4,0.7,0.4,2.6v1.8
c-1.3-0.3-2.7-0.5-4-0.5c-3.9,0-4.7,0.5-5.4,1.2c-0.6,0.6-1,1.9-1,3.8c0,2,0.4,3,1,3.6c0.7,0.7,1.4,0.8,2.9,0.8h2.5
c1.4,0,2-0.4,4-2v2h3.1V11c0-2.9-0.2-3.7-0.9-4.4C51.7,6.1,50.9,5.7,49.1,5.7z M50.1,18.3c-1,0.7-2.2,1-3.4,1h-2.3
c-0.7,0-1.1,0-1.3-0.2c-0.2-0.2-0.3-0.8-0.3-2c0-1.1,0.1-1.4,0.4-1.7c0.3-0.3,0.7-0.4,2.7-0.4h4.2L50.1,18.3L50.1,18.3z M33,1.7
h-3.2v4h-2.8v2.7h2.8v10.4c-0.1,0.9,0.1,1.8,0.7,2.5c0.7,0.6,1.6,0.9,2.4,0.8c0.7,0,1.4-0.1,2-0.2l1.8-0.5v-2.1h-2.5
c-0.8,0-1.1,0-1.2-0.1C33.1,18.9,33,18.7,33,18V8.5h3.9V5.7H33L33,1.7z M62.2,19.2c-0.8,0-1.1,0-1.2-0.1c-0.1-0.1-0.2-0.4-0.2-1.1
V8.5l0,0V0.3h-3.1v8.2h0v10.4c-0.1,0.9,0.1,1.8,0.7,2.5c0.7,0.6,1.6,0.9,2.4,0.8c0.7,0,1.4-0.1,2-0.2l1.8-0.5v-2.1H62.2L62.2,19.2
z"/>
</g>
<g id="Group_4" transform="translate(135.213 220.46)">
<path id="Path_8" class="st1" d="M29.6,25.9l-4.1,7.6L17,18.4h8.4L29.6,25.9z"/>
<path id="Path_9" class="st2" d="M25.4,18.4l5-9.2h10.1l-4.2,7.6c0,0-1,0-1.5,0c-0.2,0-0.4,0.1-0.4,0.2c-0.2,0.4-0.8,1.3-0.8,1.3
L25.4,18.4z"/>
<path id="Path_10" class="st3" d="M11.9,9.3l-1.8-3.2L9.2,7.5L5.1,0l9.8,0c0.2,0,0.4,0.1,0.5,0.3c1.3,2.3,5,8.9,5,8.9L11.9,9.3z"
/>
<path id="Path_11" class="st4" d="M23.6,36.6l1.8,3.2l0.8-1.4l4.2,7.4c0,0,0,0-0.2,0c-3.2,0-6.4,0-9.7,0c-0.1,0-0.3-0.1-0.3-0.2
l-5-9L23.6,36.6z"/>
<path id="Path_12" class="st5" d="M5.1,0l4.2,7.5l-1,1.8c0,0-7.7,0-8.3,0c0-0.1,0-0.1,0-0.2c0.3-0.5,4.1-7.5,4.9-8.9
C4.9,0.2,4.9,0.1,5.1,0C5,0,5,0,5.1,0z"/>
<path id="Path_13" class="st6" d="M40.6,27.5l-5.1,9.1h-8.3l5-9.1L40.6,27.5z"/>
<path id="Path_14" class="st7" d="M30.4,9.2c0,0,4.4-8,4.8-8.8c0.1-0.1,0.2-0.2,0.3-0.3l5.1,9.1L30.4,9.2z"/>
<path id="Path_15" class="st8" d="M40.6,9.2c0,0,5-9,5.1-9.1c1.5,2.8,5,9.1,5,9.1c0,0,0,0,0,0C50.6,9.2,43.9,9.2,40.6,9.2z"/>
<path id="Path_16" class="st9" d="M45.7,0.1l-5.1,9.1l-5.1-9.1c0,0,0-0.1,0.3-0.1c3.2,0,6.3,0,9.5,0C45.5,0,45.6,0,45.7,0.1z"/>
<path id="Path_17" class="st10" d="M27.2,36.7h8.3c0,0-4.6,8.4-5,9c0,0-0.1,0.1-0.1,0.1l-4.2-7.4L27.2,36.7z"/>
<path id="Path_18" class="st11" d="M50.7,9.2c0,0-2.8,5.2-4,7.4c-0.1,0.2-0.2,0.3-0.4,0.2c-0.5,0-1.5,0-1.5,0l-4.2-7.6L50.7,9.2z"
/>
<path id="Path_19" class="st12" d="M19.5,29.2l-4.2,7.4l-5.1-9.1l8.3,0L19.5,29.2z"/>
<path id="Path_20" class="st13" d="M25.4,18.4H17l-0.9-1.6l4.2-7.6L25.4,18.4z"/>
<path id="Path_21" class="st14" d="M40.6,27.5L36.4,20c0.1-0.1,0.2-0.1,0.3-0.1h8.1L40.6,27.5z"/>
<path id="Path_22" class="st7" d="M44.8,16.8h-8.4l4.2-7.6L44.8,16.8z"/>
<path id="Path_23" class="st15" d="M16.1,16.8l-4.2-7.5l8.4-0.1L16.1,16.8z"/>
<path id="Path_24" class="st16" d="M36.4,20l4.2,7.5l-8.4,0c0,0,2.9-5.3,4-7.3C36.3,20.1,36.3,20.1,36.4,20z"/>
<path id="Path_25" class="st17" d="M14.4,20c0.2,0.4,4.2,7.5,4.2,7.5l-8.3,0C10.2,27.5,14.3,20.1,14.4,20z"/>
<path id="Path_26" class="st18" d="M25.4,18.4l8.3,0l-4.1,7.5L25.4,18.4z"/>
<path id="Path_27" class="st19" d="M19.5,29.2l4.1,7.4l-8.3,0L19.5,29.2z"/>
<path id="Path_28" class="st20" d="M6,20l4.2,7.5l4.2-7.5L6,20z"/>
<path id="Path_29" class="st21" d="M0,9.2l8.3,0l-4.1,7.5L0,9.2z"/>
<path id="Path_30" class="st3" d="M14.4,20L6,20l4.1-7.6L14.4,20z"/>
</g>
</g>
<g>
<path class="st0" d="M218.2,16.4h-2v5.5h-1.4v-5.5h-2v-1.1h5.4V16.4z"/>
<path class="st0" d="M220.7,15.3l1.7,4.8l1.7-4.8h1.8v6.6h-1.4v-1.8l0.1-3.1l-1.8,4.9h-0.9l-1.8-4.9l0.1,3.1v1.8h-1.4v-6.6H220.7z"
/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,147 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { apiService } from '../services/api.service';
import { useNavigate } from 'react-router-dom';
const Login: React.FC = () => {
const navigate = useNavigate();
const { setIsAuthenticated } = useAuth();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [usernameError, setUsernameError] = useState('');
const [passwordError, setPasswordError] = useState('');
const [generalError, setGeneralError] = useState('');
const [loading, setLoading] = useState(true); // Start with loading true to show spinner initially
const [sessionExpiredMessage, setSessionExpiredMessage] = useState<string | null>(null);
// Simulate initial loading state or check for session expiry
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const sessionExpired = urlParams.get('sessionExpired');
if (sessionExpired) {
setSessionExpiredMessage('Session expired or something went wrong. Please log in again.');
}
// Simulate a delay to show loading spinner for a brief moment (or wait for other async checks)
setTimeout(() => {
setLoading(false); // Set loading to false after a certain period
}, 1000); // You can adjust the delay (in ms) to suit your needs
}, []);
const handleLogin = async (event: React.FormEvent) => {
event.preventDefault();
setLoading(true); // Set loading to true when login starts
let hasError = false;
if (!username.trim()) {
setUsernameError('Username is required');
hasError = true;
} else {
setUsernameError('');
}
if (!password.trim()) {
setPasswordError('Password is required');
hasError = true;
} else {
setPasswordError('');
}
if (hasError) {
setLoading(false); // If there are errors, stop the loading state
return;
}
try {
const result = await apiService.login(username, password);
if (result) {
localStorage.setItem('accessToken', result.accessToken);
localStorage.setItem('refreshToken', result.refreshToken);
localStorage.setItem('user', JSON.stringify(result.user));
localStorage.setItem('isLoggedIn', 'true');
setIsAuthenticated(true);
navigate('/', { replace: true })
} else {
setGeneralError('Invalid username or password');
}
} catch (error: any) {
setGeneralError(error.message || 'An unexpected error occurred');
} finally {
setLoading(false); // Stop loading once the login attempt is finished
}
};
// Render loading spinner initially
if (loading) {
return (
<div className="p-6 flex items-center justify-center mt-40"> {/* Adjusted margin-top */}
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<div className="min-h-screen flex flex-col bg-gray-100">
{/* Login Content */}
<div className="flex flex-1 items-center justify-center">
<div className="max-w-md w-full bg-white p-8 rounded-lg shadow">
<h2 className="text-2xl font-semibold text-center text-gray-700 mb-6">Login</h2>
<form onSubmit={handleLogin}>
{/* Username Field */}
<div className="mb-4">
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
Username
</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"
/>
{usernameError && <div className="text-red-500 text-sm mt-1">{usernameError}</div>}
</div>
{/* Password Field */}
<div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</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"
/>
{passwordError && <div className="text-red-500 text-sm mt-1">{passwordError}</div>}
</div>
{/* General Error */}
{generalError && <div className="text-red-500 text-sm mb-4">{generalError}</div>}
{/* Display the session expired message */}
{sessionExpiredMessage && <div className="text-red-500 text-sm mb-4">{sessionExpiredMessage}</div>}
{/* Login Button */}
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition duration-150"
>
Login
</button>
</form>
</div>
</div>
</div>
);
};
export default Login;

View File

@ -40,7 +40,7 @@ const Dashboard = () => {
router.diskStatus === 'DISK_CRITICAL'
).length,
diskWarnings: data.filter(router =>
router.diskUsage >= 80
router.diskUsage >= 70
).length
};

View File

@ -6,6 +6,9 @@ import RouterTable from './RouterTable';
import RouterManagement from './pages/RouterManagement';
import { RouterData } from '../../types';
import { apiService } from '../../services/api.service';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; // Import the useAuth hook
import useIdleTimeout from '../../utils/useIdleTimeout';
interface User {
name: string;
@ -15,15 +18,36 @@ interface User {
type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
const DashboardLayout: React.FC = () => {
const navigate = useNavigate();
const { isAuthenticated, setIsAuthenticated } = useAuth(); // Use AuthContext
const [activeTab, setActiveTab] = useState('dashboard');
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
const [routers, setRouters] = useState<RouterData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [user] = useState<User>({
name: 'John Doe',
role: 'Administrator'
});
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const fetchUser = async () => {
if (isAuthenticated) {
try {
const storedUser = localStorage.getItem('user');
if (storedUser) {
const { name, role }: { name: string; role: string } = JSON.parse(storedUser);
setUser({ name, role });
}
} catch (error) {
console.error('Error parsing user from localStorage:', error);
setUser(null);
}
}
};
fetchUser(); // Call the async function
}, [isAuthenticated]);
useEffect(() => {
const fetchRouters = async () => {
@ -36,11 +60,10 @@ const DashboardLayout: React.FC = () => {
data = data.filter(router => {
switch (activeFilter) {
case 'active':
return router.routerActivity?.studies &&
router.routerActivity.studies.length > 0;
return router.systemStatus.routerStatus === 'CONNECTED' &&
router.routerActivity?.studies?.some(study => study.studyStatusCode === 'Active');
case 'critical':
return router.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
router.systemStatus.appStatus === 'DISK_CRITICAL' ||
return router.systemStatus.routerStatus === 'DISCONNECTED' ||
router.diskStatus === 'DISK_CRITICAL';
case 'diskAlert':
return router.diskUsage > 80;
@ -65,27 +88,53 @@ const DashboardLayout: React.FC = () => {
return () => clearInterval(interval);
}, [activeFilter]);
const handleLogout = () => {
// Handle logout functionality
const handleLogout = async () => {
console.log('Logging out...');
// Add your logout logic here
try {
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
await apiService.logout(refreshToken); // Call logout API if available
}
// Remove only authentication-related items from localStorage
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
setUser(null); // Set user state to null after logging out
setIsAuthenticated(false); // Update the authentication state in context
// Redirect to login page
//navigate('/login');
//window.location.href = '/login';
navigate('/login', { replace: true });
} catch (error) {
console.error('Error during logout:', error);
}
};
// Use the idle timeout hook
useIdleTimeout(handleLogout);
const getSummary = (routerData: RouterData[]) => {
return {
total: routerData.length,
active: routerData.filter(r =>
r.routerActivity?.studies &&
r.routerActivity.studies.length > 0
r.systemStatus.routerStatus === 'CONNECTED' && // Router (VM, app, VPN) is up
r.routerActivity?.studies?.some(study => study.studyStatusCode === 'Active') // At least one study is active
).length,
critical: routerData.filter(r =>
r.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
r.systemStatus.appStatus === 'DISK_CRITICAL' ||
r.diskStatus === 'DISK_CRITICAL'
r.systemStatus.routerStatus === 'DISCONNECTED' || // Router (VM, app, VPN) is down
r.diskStatus === 'DISK_CRITICAL' // Disk is critical
).length,
diskAlert: routerData.filter(r => r.diskUsage > 80).length
diskAlert: routerData.filter(r => r.diskUsage > 80).length // Disk usage alert
};
};
const renderContent = () => {
if (loading) {
return (
@ -128,6 +177,7 @@ const DashboardLayout: React.FC = () => {
activeFilter === 'active' ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setActiveFilter('active')}
title="Study in transmit currently"
>
<h3 className="text-sm font-medium text-gray-500">Active Routers</h3>
<p className="text-2xl font-semibold text-green-600 mt-1">{summary.active}</p>
@ -217,13 +267,14 @@ const DashboardLayout: React.FC = () => {
<div className="min-h-screen bg-gray-100">
<Header user={user} onLogout={handleLogout} />
<div className="flex h-[calc(100vh-4rem)]">
<Navbar activeTab={activeTab} onTabChange={setActiveTab} />
<Navbar activeTab={activeTab} onTabChange={setActiveTab} role={user?.role} />
<main className="flex-1 overflow-auto">
{renderContent()}
</main>
</div>
</div>
);
};
export default DashboardLayout;

View File

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

View File

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

View File

@ -2,13 +2,13 @@
import React from 'react';
import { ChevronRight, ChevronDown } from 'lucide-react';
import { RouterData } from '../../types';
import { STATUS_COLORS, formatStatus, getStatusColor } from '../../utils/statusHelpers';
import { STATUS_COLORS, formatStatus, getStatusColor} from '../../utils/statusHelpers';
interface RouterTableRowProps {
router: RouterData;
expandedRows: Set<string>;
timeLeft: { [key: string]: number };
onToggleExpansion: (id: number, section: 'activity' | 'status' | 'disk') => void;
onToggleExpansion: (id: number, section: 'router_details' |'activity' | 'status' | 'disk') => void;
onExpandedContentHover: (id: number, section: 'activity' | 'status' | 'disk') => void;
}
@ -41,6 +41,31 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
return 'bg-green-500';
};
const renderRouterDetailsPanel = () => (
<div
className="bg-gray-50 p-4 relative"
onMouseEnter={() => onExpandedContentHover(router.id, 'router_details')}
>
<div className="h-1 bg-gray-200 absolute top-0 left-0 right-0">
<div
className="h-1 bg-blue-500 transition-all duration-200"
style={{ width: `${timeLeft[`${router.id}-router_details`] || 0}%` }}
/>
</div>
<h3 className="font-semibold mb-3">Router Details</h3>
<div className="grid grid-cols-3 gap-4">
<div className="bg-white p-3 rounded shadow-sm">
<div className="grid gap-2">
<div><span className="font-medium">Facility AET:</span> {router.facilityAET}</div>
<div><span className="font-medium">Router VM IP:</span> {router.routerVmPrimaryIp}</div>
</div>
</div>
</div>
</div>
);
const renderActivityPanel = () => (
<div
className="bg-gray-50 p-4 relative"
@ -88,6 +113,12 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<h4 className="font-semibold mb-2">VM Status</h4>
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vmStatus)}`}>
{formatStatus(router.systemStatus.vmStatus)}
</span>
</div>
<div>
<h4 className="font-semibold mb-2">VPN Status</h4>
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
@ -96,22 +127,22 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
</div>
<div>
<h4 className="font-semibold mb-2">App Status</h4>
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.diskStatus)}`}>
{formatStatus(router.diskStatus)}
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.appStatus)}`}>
{formatStatus(router.systemStatus.appStatus)}
</span>
</div>
<div>
<h4 className="font-semibold mb-2">VM Status</h4>
{router.systemStatus.vms.length > 0 ? (
router.systemStatus.vms.map((vm, idx) => (
<h4 className="font-semibold mb-2">Container Status</h4>
{router.systemStatus.containers.length > 0 ? (
router.systemStatus.containers.map((container, idx) => (
<div key={idx} className="mb-1">
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(vm.status)}`}>
VM {vm.id}: {formatStatus(vm.status)}
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(container.status)}`}>
{container.container_name}: {formatStatus(container.status)}
</span>
</div>
))
) : (
<span className="text-gray-500">No VMs configured</span>
<span className="text-gray-500">No Containers configured</span>
)}
</div>
</div>
@ -150,7 +181,19 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
<React.Fragment>
<tr className="border-b hover:bg-gray-50">
<td className="px-4 py-2">{router.slNo}</td>
<td className="px-4 py-2">{router.routerId}</td>
<td className="px-4 py-2">
<button
onClick={() => onToggleExpansion(router.id, 'router_details')}
className="flex items-center gap-1 text-blue-500 hover:text-blue-700"
>
{expandedRows.has(`${router.id}-router_details`) ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
<span>{router.routerId}</span>
</button>
</td>
<td className="px-4 py-2">{router.routerAlias}</td>
<td className="px-4 py-2">{router.facility}</td>
<td className="px-4 py-2">
@ -163,7 +206,8 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
) : (
<ChevronRight size={16} />
)}
View Activity ({router.routerActivity.studies.length} studies)
<span>{router?.routerActivity?.studies?.[0]?.studyStatusCode || "Idle"}</span>
</button>
</td>
<td className="px-4 py-2">
@ -176,8 +220,8 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
) : (
<ChevronRight size={16} />
)}
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
{formatStatus(router.systemStatus.vpnStatus)}
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.routerStatus)}`}>
{formatStatus(router.systemStatus.routerStatus)}
</span>
</button>
</td>
@ -206,6 +250,13 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
</tr>
{/* Expandable Panels */}
{expandedRows.has(`${router.id}-router_details`) && (
<tr>
<td colSpan={8}>
{renderRouterDetailsPanel()}
</td>
</tr>
)}
{expandedRows.has(`${router.id}-activity`) && (
<tr>
<td colSpan={8}>

View File

@ -1,12 +1,12 @@
// src/config/env.ts
interface Config {
apiUrl: string;
environment: string;
}
apiUrl: string;
environment: string;
}
const config: Config = {
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3001/api/v1',
environment: import.meta.env.VITE_NODE_ENV || 'development',
};
const config: Config = {
apiUrl: import.meta.env.VITE_API_URL,
environment: import.meta.env.VITE_NODE_ENV || 'development',
};
export default config;
export default config;

View File

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

View File

@ -21,6 +21,9 @@ export const MOCK_ROUTERS: RouterData[] = [
routerId: 'RTR001',
facility: 'City Hospital',
routerAlias: 'Main-Router-1',
facilityAET: 'RTR_1',
openvpnIp: '10.8.0.101',
routerVmPrimaryIp: '192.168.0.101',
lastSeen: '2024-03-07T14:30:00Z',
diskStatus: 'Normal',
diskUsage: 45,
@ -64,6 +67,9 @@ export const MOCK_ROUTERS: RouterData[] = [
routerId: 'RTR002',
facility: 'Medical Center',
routerAlias: 'Emergency-Router',
facilityAET: 'RTR_2',
openvpnIp: '10.8.0.102',
routerVmPrimaryIp: '192.168.0.102',
lastSeen: '2024-03-07T14:25:00Z',
diskStatus: 'Critical',
diskUsage: 92,
@ -97,6 +103,9 @@ export const MOCK_ROUTERS: RouterData[] = [
routerId: 'RTR003',
facility: 'Imaging Center',
routerAlias: 'Radiology-Router',
facilityAET: 'RTR_3',
openvpnIp: '10.8.0.103',
routerVmPrimaryIp: '192.168.0.103',
lastSeen: '2024-03-07T14:20:00Z',
diskStatus: 'Warning',
diskUsage: 78,

View File

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

View File

@ -1,7 +1,13 @@
// router-dashboard/src/services/api.service.ts
import { RouterData, FilterType, BackendRouter } from '../types';
const API_BASE_URL = 'http://localhost:3001/api/v1';
const API_BASE_URL = process.env.NODE_ENV === 'development'
? 'http://localhost:3000/api/v1'
: '/api/v1';
console.log("process.env.NODE_ENV", process.env.NODE_ENV);
console.log("API_BASE_URL", API_BASE_URL);
// Default request options for all API calls
const DEFAULT_OPTIONS = {
@ -11,6 +17,35 @@ const DEFAULT_OPTIONS = {
}
};
// Helper function to get Authorization header
const getAuthHeaders = () => {
const accessToken = localStorage.getItem('accessToken');
if (!accessToken) {
console.error('No access token found in localStorage');
return {};
}
return {
Authorization: `Bearer ${accessToken}`,
};
};
// Helper function to remove tokens from localStorage
const removeTokens = () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
localStorage.removeItem('isLoggedIn');
};
const redirectToLogin = (logMessage: string) => {
console.log(logMessage);
removeTokens(); // Remove all tokens if refresh token expired
// Redirect to login page with a query parameter indicating session expiry
window.location.href = '/login?sessionExpired=true'; // Redirect with the query parameter
}
// Helper function to log API responses in development
const logResponse = (prefix: string, data: any) => {
if (process.env.NODE_ENV === 'development') {
@ -19,9 +54,131 @@ const logResponse = (prefix: string, data: any) => {
};
class ApiService {
async login(username: string, password: string): Promise<{ accessToken: string; refreshToken: string; user: any } | null> {
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error('Invalid username or password');
}
const data = await response.json();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
user: data.user,
};
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
// Refresh access token using the refresh token
async refreshToken(): Promise<boolean> {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
console.error('No refresh token found in localStorage');
return false;
}
try {
const response = await fetch(`${API_BASE_URL}/auth/refresh-token`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
console.log('Failed to refresh token: Expired or invalid token');
return false;
}
const data = await response.json();
// Save the new access and refresh tokens
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
return true;
} catch (error) {
console.error('Error refreshing token:', error);
return false;
}
}
// API call with token handling for expired access token or refresh token
async fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
// Get the Authorization headers dynamically
const authOptions = getAuthHeaders();
// Merge the passed options with the Authorization header inside the headers object
const finalOptions = {
...options, // Include user-specified options like method, body, etc.
headers: {
...(options.headers || {}), // Include existing headers from the passed options
...authOptions, // Add Authorization header dynamically
},
};
// Print the request details before sending it
//console.log('Request Details:');
//console.log('URL:', url);
//console.log('Options:', JSON.stringify(finalOptions, null, 2)); // Pretty print the options
const response = await fetch(url, { ...finalOptions });
if (response.status === 401) {
const errorData = await response.json();
// Check for Access token expired
if (errorData.message === 'Access token expired') {
console.log("Access token is expired, initiating to re generate")
const refreshed = await this.refreshToken();
if (refreshed) {
// Get the Authorization headers dynamically
const authOptions = getAuthHeaders();
// Retry the original request with new access token
return fetch(url, { ...options, ...authOptions });
} else {
redirectToLogin("Refresh token is expired, removing all stored session data");
}
}
// Check for Refresh token expired
if (errorData.message === 'Refresh token expired') {
redirectToLogin("Refresh token is expired, removing all stored session data");
}
redirectToLogin(errorData.message);
}
return response;
}
async logout(refreshToken: string): Promise<void> {
try {
const response = await this.fetchWithAuth(`${API_BASE_URL}/auth/logout`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Failed to logout');
}
await response.json();
} catch (error) {
console.error('Logout error:', error);
throw error;
}
}
async getAllRouters(filter: FilterType = 'all'): Promise<RouterData[]> {
try {
const response = await fetch(`${API_BASE_URL}/routers?filter=${filter}`, {
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers?filter=${filter}`, {
method: 'GET',
...DEFAULT_OPTIONS
});
@ -58,6 +215,9 @@ class ApiService {
routerId: router.routerId, // Changed from router.router_id
facility: router.facility,
routerAlias: router.routerAlias, // Changed from router.router_alias
facilityAET: router.facilityAET, // Changed from router.facility_aet
openvpnIp: router.openvpnIp, // Changed from router.openvpn_ip
routerVmPrimaryIp: router.routerVmPrimaryIp, // Changed from router.router_vm_primary_ip
lastSeen: router.lastSeen, // Changed from router.last_seen
diskStatus: router.diskStatus, // Changed from router.disk_status_code
diskUsage: router.diskUsage || 0, // Changed from router.disk_usage
@ -72,18 +232,28 @@ class ApiService {
patientName: study.patientName,
studyDate: study.studyDate,
modality: study.modality,
studyDescription: study.studyDescription
studyDescription: study.studyDescription,
studyStatusCode: study.studyStatusCode
}))
: []
},
systemStatus: {
vpnStatus: router.systemStatus?.vpnStatus || 'unknown',
appStatus: router.systemStatus?.appStatus || 'unknown',
vmStatus: router.systemStatus?.vmStatus || 'unknown',
routerStatus: router.systemStatus?.routerStatus || 'unknown',
vms: Array.isArray(router.systemStatus?.vms)
? router.systemStatus.vms.map((vm: any) => ({
id: vm.id,
status: vm.status
}))
: [],
containers: Array.isArray(router.systemStatus?.containers)
? router.systemStatus.containers.map((container: any) => ({
container_name: container.container_name,
status: container.status_code
}))
: []
}
};
@ -99,7 +269,7 @@ class ApiService {
async getRouterById(id: number): Promise<RouterData | null> {
try {
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
...DEFAULT_OPTIONS
});
if (!response.ok) {
@ -129,7 +299,7 @@ class ApiService {
last_seen: new Date().toISOString(), // Set current timestamp
};
const response = await fetch(`${API_BASE_URL}/routers`, {
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify(backendData),
@ -161,7 +331,7 @@ class ApiService {
last_seen: new Date().toISOString() // Update timestamp on changes
};
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
method: 'PUT',
...DEFAULT_OPTIONS,
body: JSON.stringify(backendData),
@ -180,7 +350,7 @@ class ApiService {
async deleteRouter(id: number): Promise<boolean> {
try {
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
method: 'DELETE',
...DEFAULT_OPTIONS
});
@ -196,7 +366,7 @@ class ApiService {
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
try {
const response = await fetch(
const response = await this.fetchWithAuth(
`${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`,
{ ...DEFAULT_OPTIONS }
);
@ -213,7 +383,7 @@ class ApiService {
async checkApiStatus(): Promise<boolean> {
try {
const response = await fetch(`${API_BASE_URL}/routers`, {
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers`, {
method: 'GET',
...DEFAULT_OPTIONS
});

View File

@ -7,6 +7,7 @@ export interface BackendStudy {
study_date: string;
modality: string;
study_description: string;
study_status_code: string;
}
export interface BackendVM {
@ -21,6 +22,9 @@ export interface BackendStudy {
router_id: string;
facility: string;
router_alias: string;
facility_aet: string;
openvpn_ip: string;
router_vm_primary_ip: string;
last_seen: string;
disk_status_code: string;
disk_usage: number;

View File

@ -7,6 +7,7 @@ export interface Study {
studyDate: string;
modality: string;
studyDescription: string;
studyStatusCode: string;
}
export interface VM {
@ -14,6 +15,11 @@ export interface VM {
status: string;
}
export interface Container {
container_name: string;
status_code: string;
}
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
export interface RouterData {
@ -22,6 +28,9 @@ export interface RouterData {
routerId: string;
facility: string;
routerAlias: string;
facilityAET: string;
openvpnIp: string;
routerVmPrimaryIp: string;
lastSeen: string;
diskStatus: string;
diskUsage: number;
@ -33,7 +42,10 @@ export interface RouterData {
systemStatus: {
vpnStatus: string;
appStatus: string;
vmStatus: string;
routerStatus: string;
vms: VM[];
containers: Container[];
};
}

View File

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

View File

@ -1,6 +1,7 @@
// src/utils/statusHelpers.ts
// Define all possible status values
// Below's are for demo purpose
export type StatusType =
| 'RUNNING'
| 'STOPPED'
@ -8,11 +9,16 @@ export type StatusType =
| 'CONNECTED'
| 'DISCONNECTED'
| 'ERROR'
| 'UNKNOWN';
| 'UNKNOWN'
| 'ONLINE'
| 'OFFLINE';
export const STATUS_COLORS: Record<StatusType | string, string> = {
'RUNNING': 'bg-green-100 text-green-700',
'CONNECTED': 'bg-green-100 text-green-700',
'ONLINE': 'bg-green-100 text-green-700',
'OFFLINE': 'bg-red-100 text-red-700',
'STOPPED': 'bg-red-100 text-red-700',
'DISCONNECTED': 'bg-red-100 text-red-700',
'WARNING': 'bg-yellow-100 text-yellow-700',
@ -20,11 +26,26 @@ export const STATUS_COLORS: Record<StatusType | string, string> = {
'UNKNOWN': 'bg-gray-100 text-gray-700'
};
export const formatStatus = (status: string): string => {
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
// Add this helper function
function getStatusAfterUnderscore(status: string): string {
if (status.includes('_')) {
const parts = status.split('_');
return parts[1] || ''; // Get the part after the underscore or return an empty string
}
return status; // Return the original string if no underscore is present
}
export const getStatus = (status: string): string => {
const keywords = ['CONNECTED', 'ONLINE', 'RUNNING'];
return keywords.some((keyword) => status === keyword) ? 'Up' : 'Down';
};
// Add this helper function
export const getStatusColor = (status: string): string => {
return STATUS_COLORS[status] || STATUS_COLORS['UNKNOWN'];
return STATUS_COLORS[getStatusAfterUnderscore(status)] || STATUS_COLORS['UNKNOWN'];
};
export const formatStatus = (status: string): string => {
return getStatus(getStatusAfterUnderscore(status));
};

View File

@ -0,0 +1,62 @@
import { useEffect, useRef } from 'react';
/**
* Custom hook to manage user inactivity and trigger a timeout action.
* @param onTimeout - Function to execute when the timeout is reached.
* @param timeout - Duration (in ms) before timeout. Default: 30 minutes.
*/
const useIdleTimeout = (onTimeout: () => void, timeout: number = 30 * 60 * 1000) => {
// Ref to persist the timeout timer and prevent resets during re-renders
const timeoutTimerRef = useRef<NodeJS.Timeout | null>(null);
// Function to reset the timer
const resetTimer = () => {
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current);
}
timeoutTimerRef.current = setTimeout(onTimeout, timeout);
};
useEffect(() => {
// Event listener for user activity (mousemove, keydown, click)
const handleUserActivity = () => {
resetTimer();
};
// Event listener for tab visibility (page hidden or visible)
const handleVisibilityChange = () => {
if (document.hidden) {
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current); // Pause the timer if the tab is not visible
}
} else {
resetTimer(); // Restart the timer when the tab becomes visible again
}
};
// Add event listeners for user activity
window.addEventListener('mousemove', handleUserActivity);
window.addEventListener('keydown', handleUserActivity);
window.addEventListener('click', handleUserActivity);
// Add event listener for page visibility change
document.addEventListener('visibilitychange', handleVisibilityChange);
// Initialize the timer when the component mounts
resetTimer();
// Cleanup function to remove listeners and clear the timer
return () => {
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current); // Cleanup the timeout
}
window.removeEventListener('mousemove', handleUserActivity);
window.removeEventListener('keydown', handleUserActivity);
window.removeEventListener('click', handleUserActivity);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [onTimeout, timeout]); // Dependency array to only re-run on changes to onTimeout or timeout
};
export default useIdleTimeout;

View File

@ -1,267 +0,0 @@
DELIMITER //
CREATE PROCEDURE seed_complete_router_system()
BEGIN
-- Disable foreign key checks and start fresh
SET FOREIGN_KEY_CHECKS=0;
-- Conditionally clear existing data, only if the table exists
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'auth_log') THEN
TRUNCATE TABLE auth_log;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_sessions') THEN
TRUNCATE TABLE user_sessions;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_router_access') THEN
TRUNCATE TABLE user_router_access;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'users') THEN
TRUNCATE TABLE users;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'container_status_history') THEN
TRUNCATE TABLE container_status_history;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_status_history') THEN
TRUNCATE TABLE router_status_history;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'container_status') THEN
TRUNCATE TABLE container_status;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'vm_details') THEN
TRUNCATE TABLE vm_details;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'dicom_study_overview') THEN
TRUNCATE TABLE dicom_study_overview;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'system_status') THEN
TRUNCATE TABLE system_status;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_settings_history') THEN
TRUNCATE TABLE router_settings_history;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_settings') THEN
TRUNCATE TABLE router_settings;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'routers') THEN
TRUNCATE TABLE routers;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'status_type') THEN
TRUNCATE TABLE status_type;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'status_category') THEN
TRUNCATE TABLE status_category;
END IF;
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS=1;
-- Insert status categories
INSERT INTO status_category (name, description)
VALUES
('Network', 'Network related statuses'),
('Disk', 'Disk related statuses'),
('VPN', 'VPN connection statuses'),
('License', 'License statuses'),
('Container', 'Container related statuses')
ON DUPLICATE KEY UPDATE id = id;
-- Insert status types
INSERT INTO status_type (category_id, name, code, description, severity)
VALUES
(1, 'Online', 'NET_ONLINE', 'System is online', 1),
(1, 'Offline', 'NET_OFFLINE', 'System is offline', 5),
(2, 'Normal', 'DISK_NORMAL', 'Disk usage is normal', 1),
(2, 'Warning', 'DISK_WARNING', 'Disk usage is high', 3),
(2, 'Critical', 'DISK_CRITICAL', 'Disk usage is critical', 5),
(3, 'Connected', 'VPN_CONNECTED', 'VPN is connected', 1),
(3, 'Disconnected', 'VPN_DISCONNECTED', 'VPN is disconnected', 5),
(5, 'Running', 'CONTAINER_RUNNING', 'Container is running', 1),
(5, 'Stopped', 'CONTAINER_STOPPED', 'Container is stopped', 5)
ON DUPLICATE KEY UPDATE id = id;
-- Insert routers
INSERT INTO routers (router_id, facility, router_alias, last_seen, vpn_status_code, disk_status_code, license_status, free_disk, total_disk, disk_usage)
VALUES
('RTR001', 'Main Hospital', 'MAIN_RAD', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'active', 500000000000, 1000000000000, 50.00),
('RTR002', 'Emergency Center', 'ER_RAD', NOW(), 'VPN_CONNECTED', 'DISK_WARNING', 'active', 400000000000, 1000000000000, 60.00),
('RTR003', 'Imaging Center', 'IMG_CENTER', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'active', 600000000000, 1000000000000, 40.00)
ON DUPLICATE KEY UPDATE id = id;
-- Store router IDs for later use
SET @router1_id = (SELECT id FROM routers WHERE router_id = 'RTR001');
SET @router2_id = (SELECT id FROM routers WHERE router_id = 'RTR002');
SET @router3_id = (SELECT id FROM routers WHERE router_id = 'RTR003');
-- Insert system status
INSERT INTO system_status (router_id)
VALUES
(@router1_id),
(@router2_id),
(@router3_id)
ON DUPLICATE KEY UPDATE id = id;
-- Insert container status
INSERT INTO container_status (system_status_id, container_number, status_code)
VALUES
(1, 1, 'CONTAINER_RUNNING'),
(1, 2, 'CONTAINER_RUNNING'),
(2, 1, 'CONTAINER_RUNNING'),
(2, 2, 'CONTAINER_STOPPED'),
(3, 1, 'CONTAINER_RUNNING')
ON DUPLICATE KEY UPDATE id = id;
-- Insert VM details
INSERT INTO vm_details (router_id, vm_number, status_code)
VALUES
(@router1_id, 1, 'NET_ONLINE'),
(@router2_id, 1, 'NET_ONLINE'),
(@router3_id, 1, 'NET_ONLINE')
ON DUPLICATE KEY UPDATE id = id;
-- Insert DICOM studies
INSERT INTO dicom_study_overview (
router_id,
study_instance_uid,
patient_id,
patient_name,
accession_number,
study_date,
modality,
study_description,
series_instance_uid,
procedure_code,
referring_physician_name
)
VALUES
(@router1_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.134', 'P1', 'John Doe', 'ACC1234', '2024-03-15', 'CT', 'Chest CT', '1.2.840.113619.2.55.3.283116435.276.1543707219.135', 'CT001', 'Dr. Smith'),
(@router2_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.136', 'P2', 'Jane Doe', 'ACC1235', '2024-03-15', 'MR', 'Brain MRI', '1.2.840.113619.2.55.3.283116435.276.1543707219.137', 'MR001', 'Dr. Johnson')
ON DUPLICATE KEY UPDATE id = id;
-- Insert router settings for each router (calls to upsert_router_settings are disabled)
-- Main Hospital Router
-- CALL upsert_router_settings(
-- @router1_id,
-- 'client',
-- '{
-- "dicom": {
-- "local": {
-- "aet": "MAIN_RAD",
-- "port": 104,
-- "file_directory": "/dicom_images",
-- "wait_time": 2,
-- "receiver_wait_time": 5000
-- },
-- "association": {
-- "acse_timeout": 5,
-- "dimse_timeout": 1000,
-- "network_timeout": 1000,
-- "retry": {
-- "attempts": 3,
-- "interval": 10
-- }
-- }
-- },
-- "rabbitmq": {
-- "local": {
-- "hostname": "router-rabbitmq",
-- "port": 5672,
-- "credentials": {
-- "username": "vitalengine",
-- "password": "vitalengine"
-- },
-- "settings": {
-- "durable": true,
-- "auto_delete": false,
-- "exchange_type": "direct",
-- "heartbeat": 50
-- }
-- }
-- },
-- "scp_connections": {
-- "pacs_nodes": [
-- {
-- "host": "pacsmain.example.com",
-- "port": 104
-- },
-- {
-- "host": "pacsbackup.example.com",
-- "port": 104
-- }
-- ]
-- }
-- }',
-- 'system',
-- 'Initial client configuration for Main Hospital'
-- );
-- Emergency Center Router
-- CALL upsert_router_settings(
-- @router2_id,
-- 'client',
-- '{
-- "dicom": {
-- "local": {
-- "aet": "ER_RAD",
-- "port": 104,
-- "file_directory": "/dicom_images",
-- "wait_time": 2,
-- "receiver_wait_time": 5000
-- },
-- "association": {
-- "acse_timeout": 5,
-- "dimse_timeout": 1000,
-- "network_timeout": 1000,
-- "retry": {
-- "attempts": 3,
-- "interval": 10
-- }
-- }
-- },
-- "rabbitmq": {
-- "local": {
-- "hostname": "router-rabbitmq",
-- "port": 5672,
-- "credentials": {
-- "username": "vitalengine",
-- "password": "vitalengine"
-- },
-- "settings": {
-- "durable": true,
-- "auto_delete": false,
-- "exchange_type": "direct",
-- "heartbeat": 50
-- }
-- }
-- },
-- "scp_connections": {
-- "pacs_nodes": [
-- {
-- "host": "pacsemergency.example.com",
-- "port": 104
-- }
-- ]
-- }
-- }',
-- 'system',
-- 'Initial client configuration for Emergency Center'
-- );
-- Insert settings for other routers as needed...
END //
DELIMITER ;

View File

@ -1,20 +1,18 @@
# Server Configuration
NODE_ENV=development
NODE_ENV=production
PORT=3000
CORS_ORIGIN=http://localhost:5173,http://localhost:3000
CORS_ORIGIN=http://${FRONTEND_IP}:5173,http://${SERVER_IP}:3000
# Database Configuration
DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=rootpassword
DB_HOST=${DB_HOST}
DB_PORT=3306
DB_USER=ve_router_user
DB_PASSWORD=ve_router_password
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

View File

@ -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"
}
}

View File

@ -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();
@ -17,7 +19,7 @@ app.use((req, res, next) => {
// CORS configuration
app.use(cors({
origin: ['http://localhost:5173', 'http://localhost:3000'],
origin: ['http://localhost:5173', 'https://router-dashboard.dev.vitalengine.com:3000', 'https://router-dashboard.vitalengine.com:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
@ -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`);

View File

@ -1,22 +1,20 @@
// src/config/config.ts
import dotenv from 'dotenv';
// Initialize dotenv at the very start
dotenv.config();
const config = {
env: process.env.NODE_ENV || 'development',
server: {
port: parseInt(process.env.PORT || '3000', 10),
// Update this to include your frontend URL
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173', 'http://localhost:3000'],
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['*'], // In production, replace with actual domains
apiPrefix: '/api/v1'
},
db: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306', 10),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '', // Make sure this is getting the password
user: process.env.DB_USER || 've_router_user',
password: process.env.DB_PASSWORD || 've_router_password',
database: process.env.DB_NAME || 've_router_db',
connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10', 10)
},
@ -26,13 +24,4 @@ const config = {
}
};
// Add this for debugging
console.log('Database Config:', {
host: config.db.host,
user: config.db.user,
database: config.db.database,
// Don't log the password
});
export default config;

View File

@ -0,0 +1,184 @@
// 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} = 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,
name: user.name,
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,
expires_at: expiresAt
});
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
name: user.name,
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,
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,
name: userData.name,
username: userData.username,
role: userData.role as UserRole,
};
const { accessToken, refreshToken: newRefreshToken} =
this.service.generateTokens(user);
const newExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days expiration
const userSessionData = {
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,
name: user.name,
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.status(200).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,38 +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';
interface ApiError {
message: string;
code?: string;
stack?: string;
}
import { Pool } from 'mysql2/promise';
export class DicomStudyController {
private service: DicomStudyService;
private commonService: CommonService;
constructor() {
this.service = new DicomStudyService();
}
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;
constructor(pool:Pool) {
this.service = new DicomStudyService(pool);
this.commonService = new CommonService();
}
getAllStudies = async (req: Request, res: Response, next: NextFunction) => {
@ -40,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 });
}
};
@ -59,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 });
}
};
@ -74,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' });
@ -117,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) {
@ -153,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 });
}
};
@ -172,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 });
}
};
@ -203,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 });
}
};

View File

@ -1,77 +1,80 @@
// src/controllers/RouterController.ts
import { Request, Response } from 'express';
import { RouterService } from '../services/RouterService';
import { RouterService, DicomStudyService, UtilityService } from '../services';
import { Pool } from 'mysql2/promise';
import { RouterData, VMUpdate, VMUpdateRequest } from '../types';
import logger from '../utils/logger';
export class RouterController {
private service: RouterService;
private dicomStudyService: DicomStudyService;
private utilityService: UtilityService;
constructor(pool: Pool) {
this.service = new RouterService(pool);
}
this.dicomStudyService = new DicomStudyService(pool);
this.utilityService = new UtilityService();
}
// src/controllers/RouterController.ts
// src/controllers/RouterController.ts
updateRouterVMs = async (req: Request, res: Response) => {
try {
const routerId = req.query.router_id as string;
const { vms } = req.body;
// src/controllers/RouterController.ts
updateRouterVMs = async (req: Request, res: Response) => {
try {
const routerId = req.query.router_id as string;
const { vms } = req.body;
// Add debugging logs
console.log('Received request:');
console.log('Router ID:', routerId);
console.log('VMs data:', vms);
// Add debugging logs
console.log('Received request:');
console.log('Router ID:', routerId);
console.log('VMs data:', vms);
if (!routerId) {
return res.status(400).json({ error: 'router_id is required' });
}
if (!routerId) {
return res.status(400).json({ error: 'router_id is required' });
}
if (!Array.isArray(vms)) {
return res.status(400).json({ error: 'VMs must be an array' });
}
if (!Array.isArray(vms)) {
return res.status(400).json({ error: 'VMs must be an array' });
}
const updatedVMs = await this.service.updateRouterVMs(routerId, vms);
res.json(updatedVMs);
} catch (err) {
// Type cast the error
const error = err as Error;
const updatedVMs = await this.service.updateRouterVMs(routerId, vms);
res.json(updatedVMs);
} catch (err) {
// Type cast the error
const error = err as Error;
// Enhanced error logging
console.error('Error in updateRouterVMs:', {
message: error?.message || 'Unknown error',
stack: error?.stack,
type: error?.constructor.name
});
// Enhanced error logging
console.error('Error in updateRouterVMs:', {
message: error?.message || 'Unknown error',
stack: error?.stack,
type: error?.constructor.name
});
res.status(500).json({
error: 'Failed to update VMs',
details: error?.message || 'Unknown error'
});
}
};
getAllRouters = async (req: Request, res: Response) => {
try {
const routers = await this.service.getAllRouters();
res.json(routers);
} catch (error: unknown) {
if (error && typeof error === 'object' && 'message' in error) {
const e = error as { message: string }; // Type assertion here
res.status(500).json({ error: e.message });
} else {
res.status(500).json({ error: 'An unexpected error occurred' });
res.status(500).json({
error: 'Failed to update VMs',
details: error?.message || 'Unknown error'
});
}
}
};
};
getAllRouters = async (req: Request, res: Response) => {
try {
const routers = await this.service.getAllRouters();
res.json(routers);
} catch (error: unknown) {
if (error && typeof error === 'object' && 'message' in error) {
const e = error as { message: string }; // Type assertion here
res.status(500).json({ error: e.message });
} else {
res.status(500).json({ error: 'An unexpected error occurred' });
}
}
};
getRouterById = async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const id = parseInt(req.params.routerId);
const router = await this.service.getRouterById(id);
if (!router) {
@ -86,9 +89,54 @@ getAllRouters = async (req: Request, res: Response) => {
createRouter = async (req: Request, res: Response) => {
try {
const router = await this.service.createRouter(req.body);
res.status(201).json(router);
const routerMetrics = req.body;
let routerId: number;
logger.info(`Initiating to create or update router: ${routerMetrics.routerId}`);
// Check for existing router
const existingRouter = await this.service.getRouterByRouterId(routerMetrics.routerId);
if (existingRouter) {
// Update existing router
const updatedRouter = await this.service.updateRouter(existingRouter.id, routerMetrics);
if (!updatedRouter) {
return res.status(404).json({ error: 'Failed to update existing router' });
}
routerId = existingRouter.id;
} else {
// Create a new router
routerMetrics.diskUsage = ((routerMetrics.totalDisk - routerMetrics.freeDisk) / routerMetrics.totalDisk) * 100;
// Get disk status
routerMetrics.diskStatus = this.utilityService.getDiskStatus(routerMetrics.diskUsage);
const router = await this.service.createRouter(routerMetrics);
if (!router) {
return res.status(404).json({ error: 'Failed to create router' });
}
routerId = router.id;
}
// Process studies after router processing
const studies = routerMetrics.routerActivity?.studies || [];
if (Array.isArray(studies) && studies.length > 0) {
logger.info(`Processing study for router: ${routerMetrics.routerId}`);
await this.dicomStudyService.processStudies(routerId, studies);
logger.info(`Successfully processed study for router: ${routerMetrics.routerId}`);
} else {
logger.info(`No study to process for router: ${routerMetrics.routerId}`);
}
// Process containers after study processing
const containers = routerMetrics.systemStatus?.containers || [];
if (Array.isArray(containers) && containers.length > 0) {
logger.info(`Processing containers for router: ${routerMetrics.routerId}`);
const result = await this.service.processContainers(routerId, containers);
logger.info(`Successfully processed ${result.affectedRows} containers for router: ${routerId}`
);
}
res.status(201).json({message: 'Router created successfully'});
} catch (error) {
logger.error('Error creating router:', error); // Log error for debugging
res.status(500).json({ error: 'Failed to create router' });
}
};

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 './DicomStudyController';
export * from './AuthController';
// 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 './auth';
// Add more middleware exports as needed

View File

@ -3,8 +3,12 @@
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DBDicomStudy, DicomStudySearchParams } from '../types/dicom';
import pool from '../config/db';
import logger from '../utils/logger';
import { Pool } from 'mysql2/promise';
import { RowDataPacket, ResultSetHeader } from 'mysql2';
export class DicomStudyRepository {
constructor(private pool: Pool) {} // Modified constructor
private async getRouterStringId(numericId: number): Promise<string> {
try {
const [result] = await pool.query(
@ -40,10 +44,9 @@ export class DicomStudyRepository {
}
private async mapDBStudyToDicomStudy(dbStudy: DBDicomStudy): Promise<DicomStudy> {
const routerStringId = await this.getRouterStringId(dbStudy.router_id);
return {
id: dbStudy.id,
router_id: routerStringId,
router_id: dbStudy.router_id.toString(),
study_instance_uid: dbStudy.study_instance_uid,
patient_id: dbStudy.patient_id,
patient_name: dbStudy.patient_name,
@ -63,18 +66,16 @@ export class DicomStudyRepository {
async create(studyData: CreateDicomStudyDTO): Promise<DicomStudy> {
try {
// Convert string router_id to numeric id for database
const numericRouterId = await this.getRouterNumericId(studyData.router_id);
const [result] = await pool.query(
`INSERT INTO dicom_study_overview (
router_id, study_instance_uid, patient_id, patient_name,
accession_number, study_date, modality, study_description,
series_instance_uid, procedure_code, referring_physician_name,
study_status_code, association_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
study_status_code, association_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
[
numericRouterId,
studyData.router_id,
studyData.study_instance_uid,
studyData.patient_id,
studyData.patient_name,
@ -246,4 +247,26 @@ export class DicomStudyRepository {
throw new Error('Failed to search DICOM studies');
}
}
async findByStudyInstanceUid(studyInstanceUid: string): Promise<DicomStudy | null> {
try {
// Use RowDataPacket[] to align with mysql2/promise type expectations
const [rows] = await pool.query<DBDicomStudy[] & RowDataPacket[]>(`
SELECT * FROM dicom_study_overview WHERE study_instance_uid = ?`,
[studyInstanceUid]
);
// Check if rows is empty
if (rows.length === 0) {
return null;
}
// Map the first result to your DicomStudy object
return await this.mapDBStudyToDicomStudy(rows[0]);
} catch (error) {
logger.error('Error fetching DICOM study by Study Instance UID:', error);
throw new Error('Failed to fetch DICOM study');
}
}
}

View File

@ -1,5 +1,5 @@
// src/repositories/RouterRepository.ts
import { RouterData, Study, VM,VMUpdate } from '../types';
import { RouterData, Study, VM, VMUpdate, Container } from '../types';
import pool from '../config/db';
import { RowDataPacket, ResultSetHeader } from 'mysql2';
import logger from '../utils/logger';
@ -11,65 +11,65 @@ export class RouterRepository {
// src/repositories/RouterRepository.ts
// src/repositories/RouterRepository.ts
async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
console.log('Repository: Starting VM update for router:', routerId);
const connection = await this.pool.getConnection();
async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
console.log('Repository: Starting VM update for router:', routerId);
const connection = await this.pool.getConnection();
try {
await connection.beginTransaction();
console.log('Started transaction');
try {
await connection.beginTransaction();
console.log('Started transaction');
// First verify router exists
const [routers] = await connection.query(
'SELECT id FROM routers WHERE router_id = ?',
[routerId]
);
console.log('Router query result:', routers);
// First verify router exists
const [routers] = await connection.query(
'SELECT id FROM routers WHERE router_id = ?',
[routerId]
);
console.log('Router query result:', routers);
if (!Array.isArray(routers) || routers.length === 0) {
throw new Error(`Router ${routerId} not found`);
}
if (!Array.isArray(routers) || routers.length === 0) {
throw new Error(`Router ${routerId} not found`);
}
// Log each VM update
for (const vm of vms) {
console.log(`Processing VM ${vm.vm_number}`);
const [result]: any = await connection.query(
`UPDATE vm_details
SET status_code = ?,
updated_at = CURRENT_TIMESTAMP
WHERE router_id = ? AND vm_number = ?`,
[vm.status_code, routerId, vm.vm_number]
);
console.log('Update result:', result);
// Log each VM update
for (const vm of vms) {
console.log(`Processing VM ${vm.vm_number}`);
const [result]: any = await connection.query(
`UPDATE vm_details
SET status_code = ?,
updated_at = CURRENT_TIMESTAMP
WHERE router_id = ? AND vm_number = ?`,
[vm.status_code, routerId, vm.vm_number]
);
console.log('Update result:', result);
if (!result.affectedRows) {
console.log('No rows affected, attempting insert');
await connection.query(
`INSERT INTO vm_details (router_id, vm_number, status_code)
VALUES (?, ?, ?)`,
[routerId, vm.vm_number, vm.status_code]
);
}
}
if (!result.affectedRows) {
console.log('No rows affected, attempting insert');
await connection.query(
`INSERT INTO vm_details (router_id, vm_number, status_code)
VALUES (?, ?, ?)`,
[routerId, vm.vm_number, vm.status_code]
);
}
}
await connection.commit();
console.log('Transaction committed');
await connection.commit();
console.log('Transaction committed');
const [updatedVMs] = await connection.query(
`SELECT * FROM vm_details WHERE router_id = ? ORDER BY vm_number`,
[routerId]
);
return updatedVMs;
const [updatedVMs] = await connection.query(
`SELECT * FROM vm_details WHERE router_id = ? ORDER BY vm_number`,
[routerId]
);
return updatedVMs;
} catch (err) {
console.error('Repository error:', err);
await connection.rollback();
throw err;
} finally {
connection.release();
console.log('Connection released');
} catch (err) {
console.error('Repository error:', err);
await connection.rollback();
throw err;
} finally {
connection.release();
console.log('Connection released');
}
}
}
private async getRouterStudies(routerId: number): Promise<Study[]> {
try {
@ -81,10 +81,12 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
patient_name as patientName,
DATE_FORMAT(study_date, '%Y-%m-%d') as studyDate,
modality,
study_description as studyDescription
study_description as studyDescription,
CONCAT(UPPER(SUBSTRING(study_status_code, 1, 1)), LOWER(SUBSTRING(study_status_code, 2))) as studyStatusCode
FROM dicom_study_overview
WHERE router_id = ?
ORDER BY study_date DESC`,
ORDER BY updated_at DESC
LIMIT 1`,
[routerId]
);
return rows as Study[];
@ -126,10 +128,78 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
}
}
private async getRouterContainers(routerId: number): Promise<Container[]> {
try {
// Then use this to query vm_details
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT
container_name,
status_code
FROM container_status
WHERE router_id = ?`,
[routerId]
);
return rows as Container[];
} catch (error) {
logger.error(`Error fetching Containers for router ${routerId}:`, error);
return [];
}
}
private async calculateVMStatus (lastSeen: string | number | Date): Promise<string> {
const currentTime = new Date();
const lastSeenTime = new Date(lastSeen);
// Use getTime() to get timestamps in milliseconds
const diffInMinutes = (currentTime.getTime() - lastSeenTime.getTime()) / (1000 * 60);
return diffInMinutes > 1 ? 'NET_OFFLINE' : 'NET_ONLINE';
};
private async calculateSystemStatus (vpnStatus: string, appStatus: string, vmStatus: string): Promise<string> {
return vpnStatus === 'VPN_CONNECTED' && appStatus === 'CONTAINER_RUNNING' && vmStatus === 'NET_ONLINE' ? 'CONNECTED' : 'DISCONNECTED';
};
async updateRouterAndContainerStatus(id: number, vpnStatus: string, appStatus: string): Promise<void> {
// Update router status
await pool.query(
`UPDATE routers SET vpn_status_code = ?, app_status_code = ?, updated_at = NOW() WHERE id = ?`,
[vpnStatus, appStatus, id]
);
// Update container status
await pool.query(
`UPDATE container_status SET status_code = ?, updated_at = NOW() WHERE router_id = ?`,
["CONTAINER_STOPPED", id]
);
}
private async transformDatabaseRouter(dbRouter: any, index: number): Promise<RouterData> {
try {
const lastSeen = new Date(dbRouter.last_seen).toISOString();
const vpnStatus = dbRouter.vpn_status_code.toString();
const appStatus = dbRouter.app_status_code.toString();
const vmStatus = await this.calculateVMStatus(lastSeen);
const routerStatus = await this.calculateSystemStatus(vpnStatus, appStatus, vmStatus);
// Update vpnStatus and appStatus based on vmStatus
let updatedVpnStatus = vpnStatus;
let updatedAppStatus = appStatus;
if (vmStatus === 'NET_OFFLINE') {
updatedVpnStatus = 'VPN_DISCONNECTED';
updatedAppStatus = 'CONTAINER_STOPPED';
await this.updateRouterAndContainerStatus(dbRouter.id, updatedVpnStatus, updatedAppStatus);
}
const studies = await this.getRouterStudies(dbRouter.id);
const vms = await this.getRouterVMs(dbRouter.id);
//const vms = await this.getRouterVMs(dbRouter.id);
const vms:VM[] = [];
const containers = await this.getRouterContainers(dbRouter.id);
return {
id: dbRouter.id,
@ -137,7 +207,10 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
routerId: dbRouter.router_id,
facility: dbRouter.facility,
routerAlias: dbRouter.router_alias,
lastSeen: new Date(dbRouter.last_seen).toISOString(),
facilityAET: dbRouter.facility_aet,
openvpnIp: dbRouter.openvpn_ip,
routerVmPrimaryIp: dbRouter.router_vm_primary_ip,
lastSeen: lastSeen,
diskStatus: dbRouter.disk_status_code,
diskUsage: parseFloat(dbRouter.disk_usage),
freeDisk: parseInt(dbRouter.free_disk),
@ -146,9 +219,12 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
studies
},
systemStatus: {
vpnStatus: dbRouter.vpn_status_code,
appStatus: dbRouter.disk_status_code,
vms
vpnStatus: updatedVpnStatus,
appStatus: updatedAppStatus,
vmStatus: vmStatus,
routerStatus: routerStatus,
vms,
containers
}
};
} catch (error) {
@ -184,6 +260,16 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
return this.transformDatabaseRouter(rows[0], 0);
}
async findByRouterId(routerId: string): Promise<RouterData | null> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM routers WHERE router_id = ?',
[routerId]
);
if (!rows.length) return null;
return this.transformDatabaseRouter(rows[0], 0);
}
async findByFacility(facility: string): Promise<RouterData[]> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM routers WHERE facility = ? ORDER BY created_at DESC',
@ -198,18 +284,22 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
}
async create(router: Partial<RouterData>): Promise<RouterData> {
const [result] = await pool.query<ResultSetHeader>(
const [result] = await pool.query<ResultSetHeader>(
`INSERT INTO routers (
router_id, facility, router_alias, last_seen,
vpn_status_code, disk_status_code, license_status,
free_disk, total_disk, disk_usage
) VALUES (?, ?, ?, NOW(), ?, ?, 'inactive', ?, ?, ?)`,
router_id, facility, router_alias, facility_aet, openvpn_ip, router_vm_primary_ip,
last_seen, vpn_status_code, disk_status_code, app_status_code,
license_status, free_disk, total_disk, disk_usage
) VALUES (?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?, 'inactive', ?, ?, ?)`,
[
router.routerId,
router.facility,
router.routerAlias,
'unknown',
router.facilityAET,
router.openvpnIp,
router.routerVmPrimaryIp,
router.systemStatus?.vpnStatus || 'unknown',
router.diskStatus || 'unknown',
router.systemStatus?.appStatus || 'unknown',
router.freeDisk,
router.totalDisk,
router.diskUsage || 0
@ -225,8 +315,14 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
if (router.routerId) updates.router_id = router.routerId;
if (router.facility) updates.facility = router.facility;
if (router.routerAlias) updates.router_alias = router.routerAlias;
if (router.facilityAET) updates.facility_aet = router.facilityAET;
if (router.openvpnIp) updates.openvpn_ip = router.openvpnIp;
if (router.routerVmPrimaryIp) updates.router_vm_primary_ip = router.routerVmPrimaryIp;
if (router.diskStatus) updates.disk_status_code = router.diskStatus;
if (router.systemStatus?.vpnStatus) updates.vpn_status_code = router.systemStatus?.vpnStatus;
if (router.systemStatus?.appStatus) updates.app_status_code = router.systemStatus?.appStatus;
if (router.freeDisk !== undefined || router.totalDisk !== undefined) {
const existingRouter = await this.findById(id);
if (!existingRouter) return null;
@ -242,7 +338,7 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
.join(', ');
await pool.query(
`UPDATE routers SET ${setClauses}, updated_at = NOW() WHERE id = ?`,
`UPDATE routers SET ${setClauses}, last_seen = NOW(), updated_at = NOW() WHERE id = ?`,
[...Object.values(updates), id]
);
}
@ -257,4 +353,37 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
);
return result.affectedRows > 0;
}
async upsertContainerStatus(routerId: string, containers: Container[]): Promise<ResultSetHeader> {
const values = containers.map((container) => [
routerId,
container.container_name,
container.status_code,
new Date(), // created_at
new Date(), // updated_at
]);
const query = `
INSERT INTO container_status (router_id, container_name, status_code, created_at, updated_at)
VALUES ${values.map(() => "(?, ?, ?, ?, ?)").join(", ")}
ON DUPLICATE KEY UPDATE
status_code = VALUES(status_code),
updated_at = VALUES(updated_at);
`;
// Flatten the values array manually (compatible with older TypeScript versions)
const flattenedValues = values.reduce((acc, val) => acc.concat(val), []);
try {
const [result] = await pool.query<ResultSetHeader>(query, flattenedValues);
return result;
} catch (error) {
logger.error("Error inserting or updating container status:");
logger.error(`Query: ${query}`);
logger.error(`Flattened Values: ${JSON.stringify(flattenedValues)}`);
logger.error("Error Details:", error);
throw new Error("Error inserting or updating container status");
}
}
}

View File

@ -0,0 +1,216 @@
// 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, refresh_token, ip_address,
user_agent, expires_at, created_at, last_activity
) VALUES (?, ?, ?, ?, ?, NOW(), NOW())`,
[
userSession.user_id,
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

@ -3,9 +3,11 @@
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();
const dicomStudyController = new DicomStudyController(pool);
// Debug logging
logger.info('Initializing DICOM routes');
@ -21,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);

View File

@ -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) => {

View File

@ -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);

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,121 @@
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: '30m' }
//{ expiresIn: '1m' }
);
};
// 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: '30m' }
//{ expiresIn: '1m' }
);
const refreshToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role, type: 'refresh' }, // Include a claim to distinguish token types
process.env.JWT_SECRET as string,
{ expiresIn: '7d' } // Longer expiry for refresh token
//{ expiresIn: '1m' }
);
return { accessToken, refreshToken };
}
// 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',
'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 = [
'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

@ -1,13 +1,14 @@
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DicomStudySearchParams } from '../types/dicom';
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DicomStudySearchParams} from '../types/dicom';
import { DicomStudyRepository } from '../repositories/DicomStudyRepository';
import pool from '../config/db';
import logger from '../utils/logger';
import { Pool } from 'mysql2/promise';
export class DicomStudyService {
private repository: DicomStudyRepository;
constructor() {
this.repository = new DicomStudyRepository();
constructor(pool: Pool) {
this.repository = new DicomStudyRepository(pool);
}
private async isValidStatusCode(statusCode: string): Promise<boolean> {
@ -39,21 +40,18 @@ export class DicomStudyService {
];
for (const field of requiredFields) {
if (!studyData[field as keyof CreateDicomStudyDTO]) {
// Check for undefined or null only (allow empty strings)
if (studyData[field as keyof CreateDicomStudyDTO] == null) {
throw new Error(`Missing required field: ${field}`);
}
}
// Commented below, currently this field is inserted with active/idle
// Validate status code
const isValidStatus = await this.isValidStatusCode(studyData.study_status_code);
if (!isValidStatus) {
throw new Error(`Invalid study status code: ${studyData.study_status_code}. Must be one of: NEW, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, ON_HOLD`);
}
// Validate date format
if (!this.isValidDate(studyData.study_date)) {
throw new Error('Invalid study date format. Use YYYY-MM-DD');
}
//const isValidStatus = await this.isValidStatusCode(studyData.study_status_code);
//if (!isValidStatus) {
//throw new Error(`Invalid study status code: ${studyData.study_status_code}. Must be one of: NEW, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, ON_HOLD`);
//}
logger.info('Creating new study', { studyData });
return await this.repository.create(studyData);
@ -151,4 +149,19 @@ export class DicomStudyService {
throw new Error('Failed to search studies');
}
}
async processStudies(routerId: number, studies: DicomStudy[]): Promise<void> {
for (const study of studies) {
const existingStudy = await this.repository.findByStudyInstanceUid(study.study_instance_uid);
if (existingStudy) {
study.router_id = existingStudy.router_id;
await this.updateStudy(existingStudy.id, study);
} else {
study.router_id = routerId.toString();
logger.info(`Inserting study for router: ${routerId}`);
await this.createStudy(study);
}
}
}
}

View File

@ -1,7 +1,8 @@
// src/services/RouterService.ts
import { RouterRepository } from '../repositories/RouterRepository';
import { RouterData,VMUpdate} from '../types';
import { Container, RouterData,VMUpdate} from '../types';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
export class RouterService {
@ -30,6 +31,10 @@ async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
return this.repository.findById(id);
}
async getRouterByRouterId(routerId: string): Promise<RouterData | null> {
return this.repository.findByRouterId(routerId);
}
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
return this.repository.findByFacility(facility);
}
@ -45,5 +50,10 @@ async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
async deleteRouter(id: number): Promise<boolean> {
return this.repository.delete(id);
}
async processContainers(routerId: number, containers: Container[]): Promise<any> {
return this.repository.upsertContainerStatus(routerId.toString(), containers);
}
}

View File

@ -0,0 +1,46 @@
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: 'Maqbool Patel', username: 'maqbool', email: 'maqbool@ve.com', password: 'maqbool@@210', role: 'admin' },
{ name: 'Kavya Raghunath', username: 'kavya', email: 'kavya@ve.com', password: 'kavya@@124', role: 'viewer' },
{ name: 'Reid McKenzie', username: 'reid', email: 'reid@ve.com', password: 'reid@@321', role: 'viewer' },
{ name: 'Guest', username: 'guest', email: 'guest@ve.com', password: 'guest@@012', 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

@ -0,0 +1,17 @@
import logger from '../utils/logger';
export class UtilityService {
// Get disk status based on disk usage (handles float values like 10.25)
getDiskStatus(diskUsage: number): string {
if (diskUsage >= 90) {
return 'DISK_CRITICAL'; // Critical disk status
} else if (diskUsage >= 70) {
return 'DISK_WARNING'; // Warning disk status
} else {
return 'DISK_NORMAL'; // Normal disk status
}
}
}

View File

@ -1,3 +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

View File

@ -72,3 +72,18 @@ export interface DicomStudySearchParams {
modality?: string;
patientName?: string;
}
export interface Study {
patientId: string;
patientName: string;
siuid: string;
accessionNumber: string;
studyDate: string;
modality: string;
studyDescription: string;
seriesInstanceUid: string;
procedureCode: string;
referringPhysicianName: string;
associationId: string;
studyStatusCode: string;
}

View File

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

View File

@ -1,33 +1,44 @@
// src/types/index.ts
export interface RouterData {
id: number; // maps to backend 'id'
slNo: number; // maps to backend 'slNo'
routerId: string; // maps to backend 'router_id'
facility: string; // maps to backend 'facility'
routerAlias: string; // maps to backend 'router_alias'
lastSeen: string; // maps to backend 'last_seen'
diskStatus: string; // maps to backend 'disk_status_code'
diskUsage: number; // maps to backend 'disk_usage'
freeDisk: number; // maps to backend 'free_disk'
totalDisk: number; // maps to backend 'total_disk'
id: number; // maps to backend 'id'
slNo: number; // maps to backend 'slNo'
routerId: string; // maps to backend 'router_id'
facility: string; // maps to backend 'facility'
routerAlias: string; // maps to backend 'router_alias'
facilityAET: string; // maps to backend 'facility_aet'
openvpnIp: string; // maps to backend 'openvpn_ip'
routerVmPrimaryIp: string; // maps to backend 'router_vm_primary_ip'
lastSeen: string; // maps to backend 'last_seen'
diskStatus: string; // maps to backend 'disk_status_code'
diskUsage: number; // maps to backend 'disk_usage'
freeDisk: number; // maps to backend 'free_disk'
totalDisk: number; // maps to backend 'total_disk'
routerActivity: {
studies: Study[];
};
systemStatus: {
vpnStatus: string; // maps to backend 'vpn_status_code'
appStatus: string; // maps to backend 'disk_status_code'
vpnStatus: string; // maps to backend 'vpn_status_code'
appStatus: string; // maps to backend 'app_status_code'
vmStatus: string; // router machine status
routerStatus: string; // overall operational status of the router
vms: VM[];
containers: Container[];
};
}
export interface Study {
siuid: string;
patientId: string;
accessionNumber: string;
patientName: string;
siuid: string;
accessionNumber: string;
studyDate: string;
modality: string;
studyDescription: string;
seriesInstanceUid: string;
procedureCode: string;
referringPhysicianName: string;
associationId: string;
studyStatusCode: string;
}
export interface VM {
@ -44,3 +55,8 @@ export interface VMUpdate {
export interface VMUpdateRequest {
vms: VMUpdate[];
}
export interface Container {
container_name: string;
status_code: string;
}

View File

@ -0,0 +1,83 @@
// 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;
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;
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;
refresh_token: string;
ip_address: string;
user_agent: string | null;
expires_at: Date;
created_at: Date;
last_activity: Date;
};
}