Compare commits
7 Commits
main
...
feature/jw
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c288da183 | |||
| 262307f17d | |||
| ea7ac4cc72 | |||
| 2047e4bbf6 | |||
| dfa974fe6b | |||
| 8630a3e2c5 | |||
| 8fe130f918 |
7
.env
Normal file
7
.env
Normal 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
25
db-scripts/00-init-db.sh
Normal 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
|
||||
@ -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...
|
||||
37
db-scripts/02-seed_common_data.sql
Normal file
37
db-scripts/02-seed_common_data.sql
Normal 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();
|
||||
49
db-scripts/03-seed_router_data_qa.sql
Normal file
49
db-scripts/03-seed_router_data_qa.sql
Normal 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
142
deploy.sh
Normal 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!"
|
||||
@ -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
38
readme.txt
Normal 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
|
||||
@ -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
|
||||
2
router-dashboard/.env.production
Normal file
2
router-dashboard/.env.production
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_API_URL=http://${SERVER_IP}:3000/api/v1
|
||||
VITE_NODE_ENV=production
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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": {
|
||||
|
||||
BIN
router-dashboard/public/ve.png
Normal file
BIN
router-dashboard/public/ve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 905 B |
@ -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;
|
||||
1
router-dashboard/src/assets/images/react-logo-tm.svg
Normal file
1
router-dashboard/src/assets/images/react-logo-tm.svg
Normal 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 |
@ -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 |
147
router-dashboard/src/components/Login.tsx
Normal file
147
router-dashboard/src/components/Login.tsx
Normal 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;
|
||||
@ -40,7 +40,7 @@ const Dashboard = () => {
|
||||
router.diskStatus === 'DISK_CRITICAL'
|
||||
).length,
|
||||
diskWarnings: data.filter(router =>
|
||||
router.diskUsage >= 80
|
||||
router.diskUsage >= 70
|
||||
).length
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -9,11 +9,15 @@ interface User {
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
user: User;
|
||||
user: User | null; // Allow user to be null
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
|
||||
if (!user) {
|
||||
return null; // Don't render the header if the user is not available
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-sm">
|
||||
<div className="h-16 px-4 flex items-center justify-between">
|
||||
|
||||
@ -13,26 +13,29 @@ import {
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
roles: string[]; // Added roles to specify allowed roles
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
||||
const [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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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;
|
||||
41
router-dashboard/src/contexts/AuthContext.tsx
Normal file
41
router-dashboard/src/contexts/AuthContext.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
setIsAuthenticated: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider: React.FC = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
|
||||
// Check localStorage on initial load
|
||||
return localStorage.getItem('isLoggedIn') === 'true';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Sync authentication state with localStorage
|
||||
if (isAuthenticated) {
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
} else {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
21
router-dashboard/src/types/user.ts
Normal file
21
router-dashboard/src/types/user.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// User Role enum
|
||||
export type UserRole = 'admin' | 'operator' | 'viewer' | 'api';
|
||||
|
||||
// User Status enum
|
||||
export type UserStatus = 'active' | 'locked' | 'disabled';
|
||||
|
||||
// User Interface
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
failed_login_attempts: number;
|
||||
last_login: Date | null;
|
||||
password_changed_at: Date;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
@ -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));
|
||||
};
|
||||
|
||||
|
||||
62
router-dashboard/src/utils/useIdleTimeout.ts
Normal file
62
router-dashboard/src/utils/useIdleTimeout.ts
Normal 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;
|
||||
@ -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 ;
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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`);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
184
ve-router-backend/src/controllers/AuthController.ts
Normal file
184
ve-router-backend/src/controllers/AuthController.ts
Normal 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' });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
23
ve-router-backend/src/controllers/SetupController.ts
Normal file
23
ve-router-backend/src/controllers/SetupController.ts
Normal 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 });
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
56
ve-router-backend/src/controllers/UserController.ts
Normal file
56
ve-router-backend/src/controllers/UserController.ts
Normal 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) => {
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './RouterController';
|
||||
export * from './DicomStudyController';
|
||||
export * from './AuthController';
|
||||
// Add more controller exports as needed
|
||||
|
||||
71
ve-router-backend/src/middleware/auth.ts
Normal file
71
ve-router-backend/src/middleware/auth.ts
Normal 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' });
|
||||
}
|
||||
};
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './errorHandler';
|
||||
export * from './auth';
|
||||
// Add more middleware exports as needed
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
216
ve-router-backend/src/repositories/UserRepository.ts
Normal file
216
ve-router-backend/src/repositories/UserRepository.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
21
ve-router-backend/src/routes/auth.routes.ts
Normal file
21
ve-router-backend/src/routes/auth.routes.ts
Normal 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;
|
||||
@ -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);
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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);
|
||||
|
||||
10
ve-router-backend/src/routes/setup.routes.ts
Normal file
10
ve-router-backend/src/routes/setup.routes.ts
Normal 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;
|
||||
19
ve-router-backend/src/routes/user.routes.ts
Normal file
19
ve-router-backend/src/routes/user.routes.ts
Normal 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;
|
||||
121
ve-router-backend/src/services/AuthService.ts
Normal file
121
ve-router-backend/src/services/AuthService.ts
Normal 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);
|
||||
};
|
||||
|
||||
}
|
||||
25
ve-router-backend/src/services/CommonService.ts
Normal file
25
ve-router-backend/src/services/CommonService.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
46
ve-router-backend/src/services/SetupService.ts
Normal file
46
ve-router-backend/src/services/SetupService.ts
Normal 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;
|
||||
};
|
||||
|
||||
};
|
||||
50
ve-router-backend/src/services/UserService.ts
Normal file
50
ve-router-backend/src/services/UserService.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
17
ve-router-backend/src/services/UtilityService.ts
Normal file
17
ve-router-backend/src/services/UtilityService.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
6
ve-router-backend/src/types/error.ts
Normal file
6
ve-router-backend/src/types/error.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
code?: string;
|
||||
stack?: string;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
83
ve-router-backend/src/types/user.ts
Normal file
83
ve-router-backend/src/types/user.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user