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
|
router_id VARCHAR(10) UNIQUE NOT NULL, -- Unique router identifier
|
||||||
facility VARCHAR(50) NOT NULL,
|
facility VARCHAR(50) NOT NULL,
|
||||||
router_alias 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,
|
last_seen TIMESTAMP NOT NULL,
|
||||||
vpn_status_code VARCHAR(50) NOT NULL,
|
vpn_status_code VARCHAR(50) NOT NULL,
|
||||||
disk_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',
|
license_status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'inactive',
|
||||||
free_disk BIGINT NOT NULL CHECK (free_disk >= 0),
|
free_disk BIGINT NOT NULL CHECK (free_disk >= 0),
|
||||||
total_disk BIGINT NOT NULL CHECK (total_disk > 0),
|
total_disk BIGINT NOT NULL CHECK (total_disk > 0),
|
||||||
@ -19,81 +23,19 @@ CREATE TABLE IF NOT EXISTS routers (
|
|||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Users and Authentication tables
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
username VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
role ENUM('admin', 'operator', 'viewer') NOT NULL DEFAULT 'viewer',
|
|
||||||
status ENUM('active', 'locked', 'disabled') NOT NULL DEFAULT 'active',
|
|
||||||
failed_login_attempts INT NOT NULL DEFAULT 0,
|
|
||||||
last_login TIMESTAMP NULL,
|
|
||||||
password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT valid_email CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
|
||||||
CONSTRAINT valid_username CHECK (username REGEXP '^[A-Za-z0-9_-]{3,50}$')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- User-Router access permissions
|
|
||||||
CREATE TABLE IF NOT EXISTS user_router_access (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
router_id INT NOT NULL,
|
|
||||||
can_view BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
can_manage BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY unique_user_router_access (user_id, router_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Session management
|
|
||||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
session_token VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
refresh_token VARCHAR(255) NOT NULL,
|
|
||||||
ip_address VARCHAR(45) NOT NULL,
|
|
||||||
user_agent TEXT,
|
|
||||||
expires_at TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL 24 HOUR),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT unique_session_token UNIQUE(session_token),
|
|
||||||
CONSTRAINT unique_refresh_token UNIQUE(refresh_token)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 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
|
-- Container status table
|
||||||
CREATE TABLE IF NOT EXISTS container_status (
|
CREATE TABLE IF NOT EXISTS container_status (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
system_status_id INT NOT NULL,
|
router_id varchar(50) NOT NULL,
|
||||||
container_number INT NOT NULL CHECK (container_number BETWEEN 1 AND 10),
|
container_name varchar(50) NOT NULL,
|
||||||
status_code VARCHAR(50) NOT NULL,
|
status_code varchar(50) NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
);
|
UNIQUE(router_id, container_name)
|
||||||
|
|
||||||
-- 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)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- DICOM study overview table with router_id as a string reference
|
-- DICOM study overview table with router_id as a string reference
|
||||||
CREATE TABLE IF NOT EXISTS dicom_study_overview (
|
CREATE TABLE IF NOT EXISTS dicom_study_overview (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
router_id VARCHAR(10) NOT NULL, -- Matching VARCHAR(10) with the routers table
|
router_id VARCHAR(10) NOT NULL, -- Matching VARCHAR(10) with the routers table
|
||||||
study_instance_uid VARCHAR(100) UNIQUE NOT NULL,
|
study_instance_uid VARCHAR(100) UNIQUE NOT NULL,
|
||||||
@ -110,7 +52,7 @@ CREATE TABLE IF NOT EXISTS dicom_study_overview (
|
|||||||
association_id VARCHAR(50) NOT NULL DEFAULT 'NEW',
|
association_id VARCHAR(50) NOT NULL DEFAULT 'NEW',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create tables if they don't exist
|
-- Create tables if they don't exist
|
||||||
CREATE TABLE IF NOT EXISTS status_type (
|
CREATE TABLE IF NOT EXISTS status_type (
|
||||||
@ -118,7 +60,7 @@ CREATE TABLE IF NOT EXISTS status_type (
|
|||||||
category_id VARCHAR(50),
|
category_id VARCHAR(50),
|
||||||
name VARCHAR(100),
|
name VARCHAR(100),
|
||||||
code VARCHAR(100),
|
code VARCHAR(100),
|
||||||
description VARCHAR(20),
|
description VARCHAR(150),
|
||||||
severity INT
|
severity INT
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -141,5 +83,67 @@ CREATE TABLE IF NOT EXISTS status_category (
|
|||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- User authentication and authorization
|
||||||
|
-- Users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('admin', 'operator', 'viewer', 'api') NOT NULL DEFAULT 'viewer',
|
||||||
|
status ENUM('active', 'locked', 'disabled') NOT NULL DEFAULT 'active',
|
||||||
|
failed_login_attempts INT NOT NULL DEFAULT 0,
|
||||||
|
last_login TIMESTAMP NULL,
|
||||||
|
password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT valid_email CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
|
||||||
|
CONSTRAINT valid_username CHECK (username REGEXP '^[A-Za-z0-9_-]{3,50}$')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Session management
|
||||||
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
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...
|
-- 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:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./router-dashboard
|
context: ./router-dashboard
|
||||||
dockerfile: Dockerfile
|
dockerfile: dockerfile
|
||||||
|
args:
|
||||||
|
- SERVER_IP=${SERVER_IP:-localhost}
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "${FRONTEND_PORT:-5173}:5173"
|
||||||
environment:
|
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:
|
depends_on:
|
||||||
- backend
|
backend:
|
||||||
volumes:
|
condition: service_healthy
|
||||||
- ./router-dashboard:/app
|
container_name: router_dashboard_frontend
|
||||||
- /app/node_modules
|
networks:
|
||||||
|
- app_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://${SERVER_IP:-localhost}:5173"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./ve-router-backend
|
context: ./ve-router-backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "3001:3000"
|
- "${BACKEND_PORT:-3000}:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=${NODE_ENV:-development}
|
||||||
- DB_HOST=host.docker.internal
|
- DB_HOST=${DB_HOST:-mysql}
|
||||||
- DB_PORT=3307
|
- DB_PORT=${DB_PORT:-3306}
|
||||||
- DB_USER=ve_router_user
|
- DB_USER=${DB_USER:-ve_router_user}
|
||||||
- DB_PASSWORD=ve_router_password
|
- DB_PASSWORD=${DB_PASSWORD:-ve_router_password}
|
||||||
- DB_NAME=ve_router_db
|
- 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:
|
depends_on:
|
||||||
- mysql
|
mysql:
|
||||||
volumes:
|
condition: service_healthy
|
||||||
- ./ve-router-backend:/app
|
healthcheck:
|
||||||
- /app/node_modules
|
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:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: rootpassword
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
|
||||||
MYSQL_DATABASE: ve_router_db
|
MYSQL_DATABASE: ${DB_NAME:-ve_router_db}
|
||||||
MYSQL_USER: ve_router_user
|
MYSQL_USER: ${DB_USER:-ve_router_user}
|
||||||
MYSQL_PASSWORD: ve_router_password
|
MYSQL_PASSWORD: ${DB_PASSWORD:-ve_router_password}
|
||||||
|
ENVIRONMENT: ${NODE_ENV:-development}
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
# Correct paths for init scripts
|
- ./db-scripts:/docker-entrypoint-initdb.d:ro
|
||||||
- ./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
|
|
||||||
ports:
|
ports:
|
||||||
- "3307:3306"
|
- "${DB_PORT:-3306}:3306"
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
|
restart: always
|
||||||
healthcheck:
|
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
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- app_network
|
||||||
|
container_name: router_dashboard_mysql
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app_network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
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=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 . .
|
COPY . .
|
||||||
|
|
||||||
ENV VITE_API_URL=http://localhost:3001/api/v1
|
ENV VITE_API_URL=http://localhost:3000/api/v1
|
||||||
|
|
||||||
EXPOSE 5173
|
EXPOSE 5173
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.14.1",
|
||||||
"router-dashboard": "file:"
|
"router-dashboard": "file:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 DashboardLayout from './components/dashboard/DashboardLayout';
|
||||||
|
import { useAuth } from './contexts/AuthContext';
|
||||||
|
|
||||||
function App() {
|
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;
|
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'
|
router.diskStatus === 'DISK_CRITICAL'
|
||||||
).length,
|
).length,
|
||||||
diskWarnings: data.filter(router =>
|
diskWarnings: data.filter(router =>
|
||||||
router.diskUsage >= 80
|
router.diskUsage >= 70
|
||||||
).length
|
).length
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import RouterTable from './RouterTable';
|
|||||||
import RouterManagement from './pages/RouterManagement';
|
import RouterManagement from './pages/RouterManagement';
|
||||||
import { RouterData } from '../../types';
|
import { RouterData } from '../../types';
|
||||||
import { apiService } from '../../services/api.service';
|
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 {
|
interface User {
|
||||||
name: string;
|
name: string;
|
||||||
@ -15,15 +18,36 @@ interface User {
|
|||||||
type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
||||||
|
|
||||||
const DashboardLayout: React.FC = () => {
|
const DashboardLayout: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isAuthenticated, setIsAuthenticated } = useAuth(); // Use AuthContext
|
||||||
const [activeTab, setActiveTab] = useState('dashboard');
|
const [activeTab, setActiveTab] = useState('dashboard');
|
||||||
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
||||||
const [routers, setRouters] = useState<RouterData[]>([]);
|
const [routers, setRouters] = useState<RouterData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [user] = useState<User>({
|
const [user, setUser] = useState<User | null>(null);
|
||||||
name: 'John Doe',
|
|
||||||
role: 'Administrator'
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchRouters = async () => {
|
const fetchRouters = async () => {
|
||||||
@ -36,11 +60,10 @@ const DashboardLayout: React.FC = () => {
|
|||||||
data = data.filter(router => {
|
data = data.filter(router => {
|
||||||
switch (activeFilter) {
|
switch (activeFilter) {
|
||||||
case 'active':
|
case 'active':
|
||||||
return router.routerActivity?.studies &&
|
return router.systemStatus.routerStatus === 'CONNECTED' &&
|
||||||
router.routerActivity.studies.length > 0;
|
router.routerActivity?.studies?.some(study => study.studyStatusCode === 'Active');
|
||||||
case 'critical':
|
case 'critical':
|
||||||
return router.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
|
return router.systemStatus.routerStatus === 'DISCONNECTED' ||
|
||||||
router.systemStatus.appStatus === 'DISK_CRITICAL' ||
|
|
||||||
router.diskStatus === 'DISK_CRITICAL';
|
router.diskStatus === 'DISK_CRITICAL';
|
||||||
case 'diskAlert':
|
case 'diskAlert':
|
||||||
return router.diskUsage > 80;
|
return router.diskUsage > 80;
|
||||||
@ -65,27 +88,53 @@ const DashboardLayout: React.FC = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [activeFilter]);
|
}, [activeFilter]);
|
||||||
|
|
||||||
const handleLogout = () => {
|
// Handle logout functionality
|
||||||
|
const handleLogout = async () => {
|
||||||
console.log('Logging out...');
|
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[]) => {
|
const getSummary = (routerData: RouterData[]) => {
|
||||||
return {
|
return {
|
||||||
total: routerData.length,
|
total: routerData.length,
|
||||||
active: routerData.filter(r =>
|
active: routerData.filter(r =>
|
||||||
r.routerActivity?.studies &&
|
r.systemStatus.routerStatus === 'CONNECTED' && // Router (VM, app, VPN) is up
|
||||||
r.routerActivity.studies.length > 0
|
r.routerActivity?.studies?.some(study => study.studyStatusCode === 'Active') // At least one study is active
|
||||||
).length,
|
).length,
|
||||||
critical: routerData.filter(r =>
|
critical: routerData.filter(r =>
|
||||||
r.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
|
r.systemStatus.routerStatus === 'DISCONNECTED' || // Router (VM, app, VPN) is down
|
||||||
r.systemStatus.appStatus === 'DISK_CRITICAL' ||
|
r.diskStatus === 'DISK_CRITICAL' // Disk is critical
|
||||||
r.diskStatus === 'DISK_CRITICAL'
|
|
||||||
).length,
|
).length,
|
||||||
diskAlert: routerData.filter(r => r.diskUsage > 80).length
|
diskAlert: routerData.filter(r => r.diskUsage > 80).length // Disk usage alert
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -128,6 +177,7 @@ const DashboardLayout: React.FC = () => {
|
|||||||
activeFilter === 'active' ? 'ring-2 ring-blue-500' : ''
|
activeFilter === 'active' ? 'ring-2 ring-blue-500' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setActiveFilter('active')}
|
onClick={() => setActiveFilter('active')}
|
||||||
|
title="Study in transmit currently"
|
||||||
>
|
>
|
||||||
<h3 className="text-sm font-medium text-gray-500">Active Routers</h3>
|
<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>
|
<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">
|
<div className="min-h-screen bg-gray-100">
|
||||||
<Header user={user} onLogout={handleLogout} />
|
<Header user={user} onLogout={handleLogout} />
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<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">
|
<main className="flex-1 overflow-auto">
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardLayout;
|
export default DashboardLayout;
|
||||||
@ -9,11 +9,15 @@ interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
user: User;
|
user: User | null; // Allow user to be null
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
|
const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
|
||||||
|
if (!user) {
|
||||||
|
return null; // Don't render the header if the user is not available
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white shadow-sm">
|
<header className="bg-white shadow-sm">
|
||||||
<div className="h-16 px-4 flex items-center justify-between">
|
<div className="h-16 px-4 flex items-center justify-between">
|
||||||
|
|||||||
@ -13,26 +13,29 @@ import {
|
|||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
onTabChange: (tab: string) => void;
|
onTabChange: (tab: string) => void;
|
||||||
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
roles: string[]; // Added roles to specify allowed roles
|
||||||
}
|
}
|
||||||
|
|
||||||
const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange, role }) => {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
const [isPinned, setIsPinned] = useState(true);
|
const [isPinned, setIsPinned] = useState(false);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const TABS_WITH_PERMISSIONS: Tab[] = [
|
||||||
const tabs: Tab[] = [
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard, roles: ['admin', 'operator', 'viewer'] },
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
{ id: 'routers', label: 'Router Management', icon: RouterIcon, roles: ['admin', 'operator'] },
|
||||||
{ id: 'routers', label: 'Router Management', icon: RouterIcon },
|
{ id: 'users', label: 'User Management', icon: Users, roles: ['admin'] },
|
||||||
{ id: 'users', label: 'User Management', icon: Users },
|
{ id: 'settings', label: 'Settings', icon: Settings, roles: ['admin'] },
|
||||||
{ id: 'settings', label: 'Settings', icon: Settings }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const filteredTabs = TABS_WITH_PERMISSIONS.filter((tab) => tab.roles.includes(role));
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (!isPinned) {
|
if (!isPinned) {
|
||||||
setIsCollapsed(false);
|
setIsCollapsed(false);
|
||||||
@ -71,7 +74,6 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
|||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
{/* Header with title and controls */}
|
|
||||||
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
|
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div>
|
<div>
|
||||||
@ -79,7 +81,11 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
|||||||
<h2 className="text-sm font-semibold">System</h2>
|
<h2 className="text-sm font-semibold">System</h2>
|
||||||
</div>
|
</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) && (
|
{(isHovered || !isCollapsed) && (
|
||||||
<button
|
<button
|
||||||
onClick={togglePin}
|
onClick={togglePin}
|
||||||
@ -90,7 +96,9 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
|||||||
>
|
>
|
||||||
<Pin
|
<Pin
|
||||||
size={16}
|
size={16}
|
||||||
className={`transform transition-transform ${isPinned ? 'rotate-45' : ''}`}
|
className={`transform transition-transform ${
|
||||||
|
isPinned ? 'rotate-45' : ''
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -99,18 +107,14 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
|||||||
className="p-1 rounded hover:bg-gray-700 transition-colors"
|
className="p-1 rounded hover:bg-gray-700 transition-colors"
|
||||||
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
|
||||||
<ChevronRight size={16} />
|
|
||||||
) : (
|
|
||||||
<ChevronLeft size={16} />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Items */}
|
{/* Navigation Items */}
|
||||||
<nav className="flex-1 p-2">
|
<nav className="flex-1 p-2">
|
||||||
{tabs.map(tab => {
|
{filteredTabs.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -119,9 +123,11 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
|||||||
className={`
|
className={`
|
||||||
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
|
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
|
||||||
transition-colors duration-200 group
|
transition-colors duration-200 group
|
||||||
${activeTab === tab.id
|
${
|
||||||
|
activeTab === tab.id
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-600 text-white'
|
||||||
: 'text-gray-300 hover:bg-gray-800'}
|
: 'text-gray-300 hover:bg-gray-800'
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
title={isCollapsed ? tab.label : undefined}
|
title={isCollapsed ? tab.label : undefined}
|
||||||
>
|
>
|
||||||
@ -131,41 +137,6 @@ const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ChevronRight, ChevronDown } from 'lucide-react';
|
import { ChevronRight, ChevronDown } from 'lucide-react';
|
||||||
import { RouterData } from '../../types';
|
import { RouterData } from '../../types';
|
||||||
import { STATUS_COLORS, formatStatus, getStatusColor } from '../../utils/statusHelpers';
|
import { STATUS_COLORS, formatStatus, getStatusColor} from '../../utils/statusHelpers';
|
||||||
|
|
||||||
interface RouterTableRowProps {
|
interface RouterTableRowProps {
|
||||||
router: RouterData;
|
router: RouterData;
|
||||||
expandedRows: Set<string>;
|
expandedRows: Set<string>;
|
||||||
timeLeft: { [key: string]: number };
|
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;
|
onExpandedContentHover: (id: number, section: 'activity' | 'status' | 'disk') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +41,31 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
|
|||||||
return 'bg-green-500';
|
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 = () => (
|
const renderActivityPanel = () => (
|
||||||
<div
|
<div
|
||||||
className="bg-gray-50 p-4 relative"
|
className="bg-gray-50 p-4 relative"
|
||||||
@ -88,6 +113,12 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<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>
|
<div>
|
||||||
<h4 className="font-semibold mb-2">VPN Status</h4>
|
<h4 className="font-semibold mb-2">VPN Status</h4>
|
||||||
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold mb-2">App Status</h4>
|
<h4 className="font-semibold mb-2">App Status</h4>
|
||||||
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.diskStatus)}`}>
|
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.appStatus)}`}>
|
||||||
{formatStatus(router.diskStatus)}
|
{formatStatus(router.systemStatus.appStatus)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold mb-2">VM Status</h4>
|
<h4 className="font-semibold mb-2">Container Status</h4>
|
||||||
{router.systemStatus.vms.length > 0 ? (
|
{router.systemStatus.containers.length > 0 ? (
|
||||||
router.systemStatus.vms.map((vm, idx) => (
|
router.systemStatus.containers.map((container, idx) => (
|
||||||
<div key={idx} className="mb-1">
|
<div key={idx} className="mb-1">
|
||||||
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(vm.status)}`}>
|
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(container.status)}`}>
|
||||||
VM {vm.id}: {formatStatus(vm.status)}
|
{container.container_name}: {formatStatus(container.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500">No VMs configured</span>
|
<span className="text-gray-500">No Containers configured</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -150,7 +181,19 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<tr className="border-b hover:bg-gray-50">
|
<tr className="border-b hover:bg-gray-50">
|
||||||
<td className="px-4 py-2">{router.slNo}</td>
|
<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.routerAlias}</td>
|
||||||
<td className="px-4 py-2">{router.facility}</td>
|
<td className="px-4 py-2">{router.facility}</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
@ -163,7 +206,8 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRight size={16} />
|
<ChevronRight size={16} />
|
||||||
)}
|
)}
|
||||||
View Activity ({router.routerActivity.studies.length} studies)
|
|
||||||
|
<span>{router?.routerActivity?.studies?.[0]?.studyStatusCode || "Idle"}</span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
@ -176,8 +220,8 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRight size={16} />
|
<ChevronRight size={16} />
|
||||||
)}
|
)}
|
||||||
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
|
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.routerStatus)}`}>
|
||||||
{formatStatus(router.systemStatus.vpnStatus)}
|
{formatStatus(router.systemStatus.routerStatus)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -206,6 +250,13 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{/* Expandable Panels */}
|
{/* Expandable Panels */}
|
||||||
|
{expandedRows.has(`${router.id}-router_details`) && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8}>
|
||||||
|
{renderRouterDetailsPanel()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
{expandedRows.has(`${router.id}-activity`) && (
|
{expandedRows.has(`${router.id}-activity`) && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={8}>
|
<td colSpan={8}>
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
interface Config {
|
interface Config {
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3001/api/v1',
|
apiUrl: import.meta.env.VITE_API_URL,
|
||||||
environment: import.meta.env.VITE_NODE_ENV || 'development',
|
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',
|
routerId: 'RTR001',
|
||||||
facility: 'City Hospital',
|
facility: 'City Hospital',
|
||||||
routerAlias: 'Main-Router-1',
|
routerAlias: 'Main-Router-1',
|
||||||
|
facilityAET: 'RTR_1',
|
||||||
|
openvpnIp: '10.8.0.101',
|
||||||
|
routerVmPrimaryIp: '192.168.0.101',
|
||||||
lastSeen: '2024-03-07T14:30:00Z',
|
lastSeen: '2024-03-07T14:30:00Z',
|
||||||
diskStatus: 'Normal',
|
diskStatus: 'Normal',
|
||||||
diskUsage: 45,
|
diskUsage: 45,
|
||||||
@ -64,6 +67,9 @@ export const MOCK_ROUTERS: RouterData[] = [
|
|||||||
routerId: 'RTR002',
|
routerId: 'RTR002',
|
||||||
facility: 'Medical Center',
|
facility: 'Medical Center',
|
||||||
routerAlias: 'Emergency-Router',
|
routerAlias: 'Emergency-Router',
|
||||||
|
facilityAET: 'RTR_2',
|
||||||
|
openvpnIp: '10.8.0.102',
|
||||||
|
routerVmPrimaryIp: '192.168.0.102',
|
||||||
lastSeen: '2024-03-07T14:25:00Z',
|
lastSeen: '2024-03-07T14:25:00Z',
|
||||||
diskStatus: 'Critical',
|
diskStatus: 'Critical',
|
||||||
diskUsage: 92,
|
diskUsage: 92,
|
||||||
@ -97,6 +103,9 @@ export const MOCK_ROUTERS: RouterData[] = [
|
|||||||
routerId: 'RTR003',
|
routerId: 'RTR003',
|
||||||
facility: 'Imaging Center',
|
facility: 'Imaging Center',
|
||||||
routerAlias: 'Radiology-Router',
|
routerAlias: 'Radiology-Router',
|
||||||
|
facilityAET: 'RTR_3',
|
||||||
|
openvpnIp: '10.8.0.103',
|
||||||
|
routerVmPrimaryIp: '192.168.0.103',
|
||||||
lastSeen: '2024-03-07T14:20:00Z',
|
lastSeen: '2024-03-07T14:20:00Z',
|
||||||
diskStatus: 'Warning',
|
diskStatus: 'Warning',
|
||||||
diskUsage: 78,
|
diskUsage: 78,
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
// File: src/main.tsx
|
// File: src/main.tsx
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.tsx'
|
import { BrowserRouter as Router } from 'react-router-dom'; // Import BrowserRouter for routing
|
||||||
import './index.css'
|
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(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<AuthProvider> {/* Wrap the App with AuthProvider to enable authentication context */}
|
||||||
|
<Router> {/* Wrap the App with Router to enable routing */}
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</Router>
|
||||||
)
|
</AuthProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
// router-dashboard/src/services/api.service.ts
|
// router-dashboard/src/services/api.service.ts
|
||||||
import { RouterData, FilterType, BackendRouter } from '../types';
|
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
|
// Default request options for all API calls
|
||||||
const DEFAULT_OPTIONS = {
|
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
|
// Helper function to log API responses in development
|
||||||
const logResponse = (prefix: string, data: any) => {
|
const logResponse = (prefix: string, data: any) => {
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
@ -19,9 +54,131 @@ const logResponse = (prefix: string, data: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ApiService {
|
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[]> {
|
async getAllRouters(filter: FilterType = 'all'): Promise<RouterData[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/routers?filter=${filter}`, {
|
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers?filter=${filter}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
...DEFAULT_OPTIONS
|
...DEFAULT_OPTIONS
|
||||||
});
|
});
|
||||||
@ -58,6 +215,9 @@ class ApiService {
|
|||||||
routerId: router.routerId, // Changed from router.router_id
|
routerId: router.routerId, // Changed from router.router_id
|
||||||
facility: router.facility,
|
facility: router.facility,
|
||||||
routerAlias: router.routerAlias, // Changed from router.router_alias
|
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
|
lastSeen: router.lastSeen, // Changed from router.last_seen
|
||||||
diskStatus: router.diskStatus, // Changed from router.disk_status_code
|
diskStatus: router.diskStatus, // Changed from router.disk_status_code
|
||||||
diskUsage: router.diskUsage || 0, // Changed from router.disk_usage
|
diskUsage: router.diskUsage || 0, // Changed from router.disk_usage
|
||||||
@ -72,18 +232,28 @@ class ApiService {
|
|||||||
patientName: study.patientName,
|
patientName: study.patientName,
|
||||||
studyDate: study.studyDate,
|
studyDate: study.studyDate,
|
||||||
modality: study.modality,
|
modality: study.modality,
|
||||||
studyDescription: study.studyDescription
|
studyDescription: study.studyDescription,
|
||||||
|
studyStatusCode: study.studyStatusCode
|
||||||
|
|
||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
},
|
},
|
||||||
systemStatus: {
|
systemStatus: {
|
||||||
vpnStatus: router.systemStatus?.vpnStatus || 'unknown',
|
vpnStatus: router.systemStatus?.vpnStatus || 'unknown',
|
||||||
appStatus: router.systemStatus?.appStatus || 'unknown',
|
appStatus: router.systemStatus?.appStatus || 'unknown',
|
||||||
|
vmStatus: router.systemStatus?.vmStatus || 'unknown',
|
||||||
|
routerStatus: router.systemStatus?.routerStatus || 'unknown',
|
||||||
vms: Array.isArray(router.systemStatus?.vms)
|
vms: Array.isArray(router.systemStatus?.vms)
|
||||||
? router.systemStatus.vms.map((vm: any) => ({
|
? router.systemStatus.vms.map((vm: any) => ({
|
||||||
id: vm.id,
|
id: vm.id,
|
||||||
status: vm.status
|
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> {
|
async getRouterById(id: number): Promise<RouterData | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
|
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
|
||||||
...DEFAULT_OPTIONS
|
...DEFAULT_OPTIONS
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -129,7 +299,7 @@ class ApiService {
|
|||||||
last_seen: new Date().toISOString(), // Set current timestamp
|
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',
|
method: 'POST',
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
body: JSON.stringify(backendData),
|
body: JSON.stringify(backendData),
|
||||||
@ -161,7 +331,7 @@ class ApiService {
|
|||||||
last_seen: new Date().toISOString() // Update timestamp on changes
|
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',
|
method: 'PUT',
|
||||||
...DEFAULT_OPTIONS,
|
...DEFAULT_OPTIONS,
|
||||||
body: JSON.stringify(backendData),
|
body: JSON.stringify(backendData),
|
||||||
@ -180,7 +350,7 @@ class ApiService {
|
|||||||
|
|
||||||
async deleteRouter(id: number): Promise<boolean> {
|
async deleteRouter(id: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
|
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
...DEFAULT_OPTIONS
|
...DEFAULT_OPTIONS
|
||||||
});
|
});
|
||||||
@ -196,7 +366,7 @@ class ApiService {
|
|||||||
|
|
||||||
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
|
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await this.fetchWithAuth(
|
||||||
`${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`,
|
`${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`,
|
||||||
{ ...DEFAULT_OPTIONS }
|
{ ...DEFAULT_OPTIONS }
|
||||||
);
|
);
|
||||||
@ -213,7 +383,7 @@ class ApiService {
|
|||||||
|
|
||||||
async checkApiStatus(): Promise<boolean> {
|
async checkApiStatus(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/routers`, {
|
const response = await this.fetchWithAuth(`${API_BASE_URL}/routers`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
...DEFAULT_OPTIONS
|
...DEFAULT_OPTIONS
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export interface BackendStudy {
|
|||||||
study_date: string;
|
study_date: string;
|
||||||
modality: string;
|
modality: string;
|
||||||
study_description: string;
|
study_description: string;
|
||||||
|
study_status_code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BackendVM {
|
export interface BackendVM {
|
||||||
@ -21,6 +22,9 @@ export interface BackendStudy {
|
|||||||
router_id: string;
|
router_id: string;
|
||||||
facility: string;
|
facility: string;
|
||||||
router_alias: string;
|
router_alias: string;
|
||||||
|
facility_aet: string;
|
||||||
|
openvpn_ip: string;
|
||||||
|
router_vm_primary_ip: string;
|
||||||
last_seen: string;
|
last_seen: string;
|
||||||
disk_status_code: string;
|
disk_status_code: string;
|
||||||
disk_usage: number;
|
disk_usage: number;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export interface Study {
|
|||||||
studyDate: string;
|
studyDate: string;
|
||||||
modality: string;
|
modality: string;
|
||||||
studyDescription: string;
|
studyDescription: string;
|
||||||
|
studyStatusCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VM {
|
export interface VM {
|
||||||
@ -14,6 +15,11 @@ export interface VM {
|
|||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Container {
|
||||||
|
container_name: string;
|
||||||
|
status_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
|
||||||
|
|
||||||
export interface RouterData {
|
export interface RouterData {
|
||||||
@ -22,6 +28,9 @@ export interface RouterData {
|
|||||||
routerId: string;
|
routerId: string;
|
||||||
facility: string;
|
facility: string;
|
||||||
routerAlias: string;
|
routerAlias: string;
|
||||||
|
facilityAET: string;
|
||||||
|
openvpnIp: string;
|
||||||
|
routerVmPrimaryIp: string;
|
||||||
lastSeen: string;
|
lastSeen: string;
|
||||||
diskStatus: string;
|
diskStatus: string;
|
||||||
diskUsage: number;
|
diskUsage: number;
|
||||||
@ -33,7 +42,10 @@ export interface RouterData {
|
|||||||
systemStatus: {
|
systemStatus: {
|
||||||
vpnStatus: string;
|
vpnStatus: string;
|
||||||
appStatus: string;
|
appStatus: string;
|
||||||
|
vmStatus: string;
|
||||||
|
routerStatus: string;
|
||||||
vms: VM[];
|
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
|
// src/utils/statusHelpers.ts
|
||||||
|
|
||||||
// Define all possible status values
|
// Below's are for demo purpose
|
||||||
|
|
||||||
export type StatusType =
|
export type StatusType =
|
||||||
| 'RUNNING'
|
| 'RUNNING'
|
||||||
| 'STOPPED'
|
| 'STOPPED'
|
||||||
@ -8,11 +9,16 @@ export type StatusType =
|
|||||||
| 'CONNECTED'
|
| 'CONNECTED'
|
||||||
| 'DISCONNECTED'
|
| 'DISCONNECTED'
|
||||||
| 'ERROR'
|
| 'ERROR'
|
||||||
| 'UNKNOWN';
|
| 'UNKNOWN'
|
||||||
|
| 'ONLINE'
|
||||||
|
| 'OFFLINE';
|
||||||
|
|
||||||
|
|
||||||
export const STATUS_COLORS: Record<StatusType | string, string> = {
|
export const STATUS_COLORS: Record<StatusType | string, string> = {
|
||||||
'RUNNING': 'bg-green-100 text-green-700',
|
'RUNNING': 'bg-green-100 text-green-700',
|
||||||
'CONNECTED': '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',
|
'STOPPED': 'bg-red-100 text-red-700',
|
||||||
'DISCONNECTED': 'bg-red-100 text-red-700',
|
'DISCONNECTED': 'bg-red-100 text-red-700',
|
||||||
'WARNING': 'bg-yellow-100 text-yellow-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'
|
'UNKNOWN': 'bg-gray-100 text-gray-700'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatStatus = (status: string): string => {
|
// Add this helper function
|
||||||
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
|
||||||
|
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 => {
|
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
|
# Server Configuration
|
||||||
NODE_ENV=development
|
NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
CORS_ORIGIN=http://localhost:5173,http://localhost:3000
|
CORS_ORIGIN=http://${FRONTEND_IP}:5173,http://${SERVER_IP}:3000
|
||||||
|
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
DB_HOST=localhost
|
DB_HOST=${DB_HOST}
|
||||||
DB_PORT=3307
|
DB_PORT=3306
|
||||||
DB_USER=root
|
DB_USER=ve_router_user
|
||||||
DB_PASSWORD=rootpassword
|
DB_PASSWORD=ve_router_password
|
||||||
DB_NAME=ve_router_db
|
DB_NAME=ve_router_db
|
||||||
DB_CONNECTION_LIMIT=10
|
DB_CONNECTION_LIMIT=10
|
||||||
|
|
||||||
# Authentication Configuration
|
# Authentication Configuration
|
||||||
JWT_SECRET=your-super-secure-jwt-secret-key
|
JWT_SECRET=VE_Router_JWT_Secret_2024@Key
|
||||||
JWT_EXPIRES_IN=1d
|
JWT_EXPIRES_IN=1d
|
||||||
SALT_ROUNDS=10
|
SALT_ROUNDS=10
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,9 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"mysql2": "^3.2.0",
|
"mysql2": "^3.2.0",
|
||||||
"ve-router-backend": "file:",
|
"ve-router-backend": "file:",
|
||||||
"winston": "^3.16.0"
|
"winston": "^3.16.0",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"jsonwebtoken": "^9.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.13",
|
"@types/cors": "^2.8.13",
|
||||||
@ -28,6 +30,8 @@
|
|||||||
"eslint": "^8.37.0",
|
"eslint": "^8.37.0",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.0.3"
|
"typescript": "^5.0.3",
|
||||||
|
"@types/bcryptjs": "^2.4.3",
|
||||||
|
"@types/jsonwebtoken": "^9.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import config from './config/config';
|
|||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import { errorHandler } from './middleware';
|
import { errorHandler } from './middleware';
|
||||||
import logger from './utils/logger';
|
import logger from './utils/logger';
|
||||||
|
import pool from './config/db';
|
||||||
|
import { SetupService } from './services';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@ -17,7 +19,7 @@ app.use((req, res, next) => {
|
|||||||
|
|
||||||
// CORS configuration
|
// CORS configuration
|
||||||
app.use(cors({
|
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,
|
credentials: true,
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
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;
|
const port = config.server.port || 3000;
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
logger.info(`Server running on port ${port} in ${config.env} mode`);
|
logger.info(`Server running on port ${port} in ${config.env} mode`);
|
||||||
|
|||||||
@ -1,22 +1,20 @@
|
|||||||
// src/config/config.ts
|
// src/config/config.ts
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
// Initialize dotenv at the very start
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
env: process.env.NODE_ENV || 'development',
|
env: process.env.NODE_ENV || 'development',
|
||||||
server: {
|
server: {
|
||||||
port: parseInt(process.env.PORT || '3000', 10),
|
port: parseInt(process.env.PORT || '3000', 10),
|
||||||
// Update this to include your frontend URL
|
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['*'], // In production, replace with actual domains
|
||||||
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173', 'http://localhost:3000'],
|
|
||||||
apiPrefix: '/api/v1'
|
apiPrefix: '/api/v1'
|
||||||
},
|
},
|
||||||
db: {
|
db: {
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: parseInt(process.env.DB_PORT || '3306', 10),
|
port: parseInt(process.env.DB_PORT || '3306', 10),
|
||||||
user: process.env.DB_USER || 'root',
|
user: process.env.DB_USER || 've_router_user',
|
||||||
password: process.env.DB_PASSWORD || '', // Make sure this is getting the password
|
password: process.env.DB_PASSWORD || 've_router_password',
|
||||||
database: process.env.DB_NAME || 've_router_db',
|
database: process.env.DB_NAME || 've_router_db',
|
||||||
connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10', 10)
|
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;
|
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
|
// src/controllers/DicomStudyController.ts
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { DicomStudyService } from '../services/DicomStudyService';
|
import { DicomStudyService, CommonService} from '../services';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
import { Pool } from 'mysql2/promise';
|
||||||
interface ApiError {
|
|
||||||
message: string;
|
|
||||||
code?: string;
|
|
||||||
stack?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DicomStudyController {
|
export class DicomStudyController {
|
||||||
private service: DicomStudyService;
|
private service: DicomStudyService;
|
||||||
|
private commonService: CommonService;
|
||||||
|
|
||||||
constructor() {
|
constructor(pool:Pool) {
|
||||||
this.service = new DicomStudyService();
|
this.service = new DicomStudyService(pool);
|
||||||
}
|
this.commonService = new CommonService();
|
||||||
|
|
||||||
private handleError(error: unknown, message: string): ApiError {
|
|
||||||
const apiError: ApiError = {
|
|
||||||
message: message
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
logger.error(`${message}: ${error.message}`);
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
apiError.message = error.message;
|
|
||||||
apiError.stack = error.stack;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error(`${message}: Unknown error type`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllStudies = async (req: Request, res: Response, next: NextFunction) => {
|
getAllStudies = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@ -40,7 +19,7 @@ export class DicomStudyController {
|
|||||||
const studies = await this.service.getAllStudies();
|
const studies = await this.service.getAllStudies();
|
||||||
res.json(studies);
|
res.json(studies);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const apiError = this.handleError(error, 'Failed to fetch studies');
|
const apiError = this.commonService.handleError(error, 'Failed to fetch studies');
|
||||||
res.status(500).json({ error: apiError });
|
res.status(500).json({ error: apiError });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -59,7 +38,7 @@ export class DicomStudyController {
|
|||||||
|
|
||||||
res.json(study);
|
res.json(study);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const apiError = this.handleError(error, `Failed to fetch study ${req.params.id}`);
|
const apiError = this.commonService.handleError(error, `Failed to fetch study ${req.params.id}`);
|
||||||
res.status(500).json({ error: apiError });
|
res.status(500).json({ error: apiError });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -74,7 +53,7 @@ export class DicomStudyController {
|
|||||||
const studies = await this.service.getStudiesByRouterId(routerId);
|
const studies = await this.service.getStudiesByRouterId(routerId);
|
||||||
res.json(studies);
|
res.json(studies);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const apiError = this.handleError(error, `Failed to fetch studies for router ${req.params.routerId}`);
|
const apiError = this.commonService.handleError(error, `Failed to fetch studies for router ${req.params.routerId}`);
|
||||||
// If router not found, return 404
|
// If router not found, return 404
|
||||||
if (error instanceof Error && error.message.includes('Invalid router_id')) {
|
if (error instanceof Error && error.message.includes('Invalid router_id')) {
|
||||||
return res.status(404).json({ error: 'Router not found' });
|
return res.status(404).json({ error: 'Router not found' });
|
||||||
@ -117,7 +96,7 @@ export class DicomStudyController {
|
|||||||
const study = await this.service.createStudy(req.body);
|
const study = await this.service.createStudy(req.body);
|
||||||
res.status(201).json(study);
|
res.status(201).json(study);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const apiError = this.handleError(error, 'Failed to create study');
|
const apiError = this.commonService.handleError(error, 'Failed to create study');
|
||||||
|
|
||||||
// Handle specific error cases
|
// Handle specific error cases
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@ -153,7 +132,7 @@ export class DicomStudyController {
|
|||||||
|
|
||||||
res.json(study);
|
res.json(study);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const apiError = this.handleError(error, `Failed to update study ${req.params.id}`);
|
const apiError = this.commonService.handleError(error, `Failed to update study ${req.params.id}`);
|
||||||
res.status(500).json({ error: apiError });
|
res.status(500).json({ error: apiError });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -172,7 +151,7 @@ export class DicomStudyController {
|
|||||||
|
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const apiError = this.handleError(error, `Failed to delete study ${req.params.id}`);
|
const apiError = this.commonService.handleError(error, `Failed to delete study ${req.params.id}`);
|
||||||
res.status(500).json({ error: apiError });
|
res.status(500).json({ error: apiError });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -203,7 +182,7 @@ export class DicomStudyController {
|
|||||||
|
|
||||||
res.json(studies);
|
res.json(studies);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const apiError = this.handleError(error, 'Failed to search studies');
|
const apiError = this.commonService.handleError(error, 'Failed to search studies');
|
||||||
res.status(500).json({ error: apiError });
|
res.status(500).json({ error: apiError });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,20 +1,26 @@
|
|||||||
// src/controllers/RouterController.ts
|
// src/controllers/RouterController.ts
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { RouterService } from '../services/RouterService';
|
import { RouterService, DicomStudyService, UtilityService } from '../services';
|
||||||
import { Pool } from 'mysql2/promise';
|
import { Pool } from 'mysql2/promise';
|
||||||
import { RouterData, VMUpdate, VMUpdateRequest } from '../types';
|
import { RouterData, VMUpdate, VMUpdateRequest } from '../types';
|
||||||
|
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
|
||||||
export class RouterController {
|
export class RouterController {
|
||||||
private service: RouterService;
|
private service: RouterService;
|
||||||
|
private dicomStudyService: DicomStudyService;
|
||||||
|
private utilityService: UtilityService;
|
||||||
|
|
||||||
constructor(pool: Pool) {
|
constructor(pool: Pool) {
|
||||||
this.service = new RouterService(pool);
|
this.service = new RouterService(pool);
|
||||||
}
|
this.dicomStudyService = new DicomStudyService(pool);
|
||||||
|
this.utilityService = new UtilityService();
|
||||||
|
}
|
||||||
|
|
||||||
// src/controllers/RouterController.ts
|
// src/controllers/RouterController.ts
|
||||||
// src/controllers/RouterController.ts
|
// src/controllers/RouterController.ts
|
||||||
updateRouterVMs = async (req: Request, res: Response) => {
|
updateRouterVMs = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const routerId = req.query.router_id as string;
|
const routerId = req.query.router_id as string;
|
||||||
const { vms } = req.body;
|
const { vms } = req.body;
|
||||||
@ -50,10 +56,9 @@ updateRouterVMs = async (req: Request, res: Response) => {
|
|||||||
details: error?.message || 'Unknown error'
|
details: error?.message || 'Unknown error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getAllRouters = async (req: Request, res: Response) => {
|
||||||
getAllRouters = async (req: Request, res: Response) => {
|
|
||||||
try {
|
try {
|
||||||
const routers = await this.service.getAllRouters();
|
const routers = await this.service.getAllRouters();
|
||||||
res.json(routers);
|
res.json(routers);
|
||||||
@ -65,13 +70,11 @@ getAllRouters = async (req: Request, res: Response) => {
|
|||||||
res.status(500).json({ error: 'An unexpected error occurred' });
|
res.status(500).json({ error: 'An unexpected error occurred' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getRouterById = async (req: Request, res: Response) => {
|
getRouterById = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(req.params.routerId);
|
||||||
const router = await this.service.getRouterById(id);
|
const router = await this.service.getRouterById(id);
|
||||||
|
|
||||||
if (!router) {
|
if (!router) {
|
||||||
@ -86,9 +89,54 @@ getAllRouters = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
createRouter = async (req: Request, res: Response) => {
|
createRouter = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const router = await this.service.createRouter(req.body);
|
const routerMetrics = req.body;
|
||||||
res.status(201).json(router);
|
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) {
|
} catch (error) {
|
||||||
|
logger.error('Error creating router:', error); // Log error for debugging
|
||||||
res.status(500).json({ error: 'Failed to create router' });
|
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 './RouterController';
|
||||||
export * from './DicomStudyController';
|
export * from './DicomStudyController';
|
||||||
|
export * from './AuthController';
|
||||||
// Add more controller exports as needed
|
// 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 './errorHandler';
|
||||||
|
export * from './auth';
|
||||||
// Add more middleware exports as needed
|
// Add more middleware exports as needed
|
||||||
|
|||||||
@ -3,8 +3,12 @@
|
|||||||
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DBDicomStudy, DicomStudySearchParams } from '../types/dicom';
|
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DBDicomStudy, DicomStudySearchParams } from '../types/dicom';
|
||||||
import pool from '../config/db';
|
import pool from '../config/db';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
import { Pool } from 'mysql2/promise';
|
||||||
|
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
||||||
|
|
||||||
export class DicomStudyRepository {
|
export class DicomStudyRepository {
|
||||||
|
constructor(private pool: Pool) {} // Modified constructor
|
||||||
|
|
||||||
private async getRouterStringId(numericId: number): Promise<string> {
|
private async getRouterStringId(numericId: number): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
@ -40,10 +44,9 @@ export class DicomStudyRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async mapDBStudyToDicomStudy(dbStudy: DBDicomStudy): Promise<DicomStudy> {
|
private async mapDBStudyToDicomStudy(dbStudy: DBDicomStudy): Promise<DicomStudy> {
|
||||||
const routerStringId = await this.getRouterStringId(dbStudy.router_id);
|
|
||||||
return {
|
return {
|
||||||
id: dbStudy.id,
|
id: dbStudy.id,
|
||||||
router_id: routerStringId,
|
router_id: dbStudy.router_id.toString(),
|
||||||
study_instance_uid: dbStudy.study_instance_uid,
|
study_instance_uid: dbStudy.study_instance_uid,
|
||||||
patient_id: dbStudy.patient_id,
|
patient_id: dbStudy.patient_id,
|
||||||
patient_name: dbStudy.patient_name,
|
patient_name: dbStudy.patient_name,
|
||||||
@ -63,18 +66,16 @@ export class DicomStudyRepository {
|
|||||||
|
|
||||||
async create(studyData: CreateDicomStudyDTO): Promise<DicomStudy> {
|
async create(studyData: CreateDicomStudyDTO): Promise<DicomStudy> {
|
||||||
try {
|
try {
|
||||||
// Convert string router_id to numeric id for database
|
|
||||||
const numericRouterId = await this.getRouterNumericId(studyData.router_id);
|
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT INTO dicom_study_overview (
|
`INSERT INTO dicom_study_overview (
|
||||||
router_id, study_instance_uid, patient_id, patient_name,
|
router_id, study_instance_uid, patient_id, patient_name,
|
||||||
accession_number, study_date, modality, study_description,
|
accession_number, study_date, modality, study_description,
|
||||||
series_instance_uid, procedure_code, referring_physician_name,
|
series_instance_uid, procedure_code, referring_physician_name,
|
||||||
study_status_code, association_id
|
study_status_code, association_id, created_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||||
[
|
[
|
||||||
numericRouterId,
|
studyData.router_id,
|
||||||
studyData.study_instance_uid,
|
studyData.study_instance_uid,
|
||||||
studyData.patient_id,
|
studyData.patient_id,
|
||||||
studyData.patient_name,
|
studyData.patient_name,
|
||||||
@ -246,4 +247,26 @@ export class DicomStudyRepository {
|
|||||||
throw new Error('Failed to search DICOM studies');
|
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
|
// 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 pool from '../config/db';
|
||||||
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
@ -11,7 +11,7 @@ export class RouterRepository {
|
|||||||
|
|
||||||
// src/repositories/RouterRepository.ts
|
// src/repositories/RouterRepository.ts
|
||||||
// src/repositories/RouterRepository.ts
|
// src/repositories/RouterRepository.ts
|
||||||
async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
||||||
console.log('Repository: Starting VM update for router:', routerId);
|
console.log('Repository: Starting VM update for router:', routerId);
|
||||||
const connection = await this.pool.getConnection();
|
const connection = await this.pool.getConnection();
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
connection.release();
|
connection.release();
|
||||||
console.log('Connection released');
|
console.log('Connection released');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getRouterStudies(routerId: number): Promise<Study[]> {
|
private async getRouterStudies(routerId: number): Promise<Study[]> {
|
||||||
try {
|
try {
|
||||||
@ -81,10 +81,12 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
patient_name as patientName,
|
patient_name as patientName,
|
||||||
DATE_FORMAT(study_date, '%Y-%m-%d') as studyDate,
|
DATE_FORMAT(study_date, '%Y-%m-%d') as studyDate,
|
||||||
modality,
|
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
|
FROM dicom_study_overview
|
||||||
WHERE router_id = ?
|
WHERE router_id = ?
|
||||||
ORDER BY study_date DESC`,
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1`,
|
||||||
[routerId]
|
[routerId]
|
||||||
);
|
);
|
||||||
return rows as Study[];
|
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> {
|
private async transformDatabaseRouter(dbRouter: any, index: number): Promise<RouterData> {
|
||||||
try {
|
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 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 {
|
return {
|
||||||
id: dbRouter.id,
|
id: dbRouter.id,
|
||||||
@ -137,7 +207,10 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
routerId: dbRouter.router_id,
|
routerId: dbRouter.router_id,
|
||||||
facility: dbRouter.facility,
|
facility: dbRouter.facility,
|
||||||
routerAlias: dbRouter.router_alias,
|
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,
|
diskStatus: dbRouter.disk_status_code,
|
||||||
diskUsage: parseFloat(dbRouter.disk_usage),
|
diskUsage: parseFloat(dbRouter.disk_usage),
|
||||||
freeDisk: parseInt(dbRouter.free_disk),
|
freeDisk: parseInt(dbRouter.free_disk),
|
||||||
@ -146,9 +219,12 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
studies
|
studies
|
||||||
},
|
},
|
||||||
systemStatus: {
|
systemStatus: {
|
||||||
vpnStatus: dbRouter.vpn_status_code,
|
vpnStatus: updatedVpnStatus,
|
||||||
appStatus: dbRouter.disk_status_code,
|
appStatus: updatedAppStatus,
|
||||||
vms
|
vmStatus: vmStatus,
|
||||||
|
routerStatus: routerStatus,
|
||||||
|
vms,
|
||||||
|
containers
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -184,6 +260,16 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
return this.transformDatabaseRouter(rows[0], 0);
|
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[]> {
|
async findByFacility(facility: string): Promise<RouterData[]> {
|
||||||
const [rows] = await pool.query<RowDataPacket[]>(
|
const [rows] = await pool.query<RowDataPacket[]>(
|
||||||
'SELECT * FROM routers WHERE facility = ? ORDER BY created_at DESC',
|
'SELECT * FROM routers WHERE facility = ? ORDER BY created_at DESC',
|
||||||
@ -200,16 +286,20 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
async create(router: Partial<RouterData>): Promise<RouterData> {
|
async create(router: Partial<RouterData>): Promise<RouterData> {
|
||||||
const [result] = await pool.query<ResultSetHeader>(
|
const [result] = await pool.query<ResultSetHeader>(
|
||||||
`INSERT INTO routers (
|
`INSERT INTO routers (
|
||||||
router_id, facility, router_alias, last_seen,
|
router_id, facility, router_alias, facility_aet, openvpn_ip, router_vm_primary_ip,
|
||||||
vpn_status_code, disk_status_code, license_status,
|
last_seen, vpn_status_code, disk_status_code, app_status_code,
|
||||||
free_disk, total_disk, disk_usage
|
license_status, free_disk, total_disk, disk_usage
|
||||||
) VALUES (?, ?, ?, NOW(), ?, ?, 'inactive', ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?, 'inactive', ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
router.routerId,
|
router.routerId,
|
||||||
router.facility,
|
router.facility,
|
||||||
router.routerAlias,
|
router.routerAlias,
|
||||||
'unknown',
|
router.facilityAET,
|
||||||
|
router.openvpnIp,
|
||||||
|
router.routerVmPrimaryIp,
|
||||||
|
router.systemStatus?.vpnStatus || 'unknown',
|
||||||
router.diskStatus || 'unknown',
|
router.diskStatus || 'unknown',
|
||||||
|
router.systemStatus?.appStatus || 'unknown',
|
||||||
router.freeDisk,
|
router.freeDisk,
|
||||||
router.totalDisk,
|
router.totalDisk,
|
||||||
router.diskUsage || 0
|
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.routerId) updates.router_id = router.routerId;
|
||||||
if (router.facility) updates.facility = router.facility;
|
if (router.facility) updates.facility = router.facility;
|
||||||
if (router.routerAlias) updates.router_alias = router.routerAlias;
|
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.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) {
|
if (router.freeDisk !== undefined || router.totalDisk !== undefined) {
|
||||||
const existingRouter = await this.findById(id);
|
const existingRouter = await this.findById(id);
|
||||||
if (!existingRouter) return null;
|
if (!existingRouter) return null;
|
||||||
@ -242,7 +338,7 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
await pool.query(
|
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]
|
[...Object.values(updates), id]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -257,4 +353,37 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
);
|
);
|
||||||
return result.affectedRows > 0;
|
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 express from 'express';
|
||||||
import { DicomStudyController } from '../controllers/DicomStudyController';
|
import { DicomStudyController } from '../controllers/DicomStudyController';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
import pool from '../config/db'; // If using default export
|
||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const dicomStudyController = new DicomStudyController();
|
const dicomStudyController = new DicomStudyController(pool);
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
logger.info('Initializing DICOM routes');
|
logger.info('Initializing DICOM routes');
|
||||||
@ -21,6 +23,9 @@ router.get('/test', (req, res) => {
|
|||||||
res.json({ message: 'DICOM routes are working' });
|
res.json({ message: 'DICOM routes are working' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
router.use(authMiddleware(pool));
|
||||||
|
|
||||||
router.post('/', (req, res, next) => {
|
router.post('/', (req, res, next) => {
|
||||||
logger.debug('POST / route hit with body:', req.body);
|
logger.debug('POST / route hit with body:', req.body);
|
||||||
dicomStudyController.createStudy(req, res, next);
|
dicomStudyController.createStudy(req, res, next);
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { Router } from 'express';
|
|||||||
import routerRoutes from './router.routes';
|
import routerRoutes from './router.routes';
|
||||||
import dicomRoutes from './dicom.routes';
|
import dicomRoutes from './dicom.routes';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
import authRoutes from './auth.routes';
|
||||||
|
import userRoutes from './user.routes';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -10,9 +12,13 @@ const router = Router();
|
|||||||
logger.info('Registering routes:');
|
logger.info('Registering routes:');
|
||||||
logger.info('- /routers -> router routes');
|
logger.info('- /routers -> router routes');
|
||||||
logger.info('- /studies -> dicom routes');
|
logger.info('- /studies -> dicom routes');
|
||||||
|
logger.info('- /auth -> auth routes');
|
||||||
|
logger.info('- /users -> user routes');
|
||||||
|
|
||||||
router.use('/routers', routerRoutes);
|
router.use('/routers', routerRoutes);
|
||||||
router.use('/studies', dicomRoutes);
|
router.use('/studies', dicomRoutes);
|
||||||
|
router.use('/auth', authRoutes);
|
||||||
|
router.use('/users', userRoutes);
|
||||||
|
|
||||||
// Debug middleware to log incoming requests
|
// Debug middleware to log incoming requests
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import pool from '../config/db'; // If using default export
|
import pool from '../config/db'; // If using default export
|
||||||
import { RouterController } from '../controllers/RouterController';
|
import { RouterController } from '../controllers/RouterController';
|
||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -9,6 +10,9 @@ import { RouterController } from '../controllers/RouterController';
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
const controller = new RouterController(pool);
|
const controller = new RouterController(pool);
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
router.use(authMiddleware(pool));
|
||||||
|
|
||||||
router.put('/vms', async (req, res, next) => {
|
router.put('/vms', async (req, res, next) => {
|
||||||
console.log('Route handler: /vms endpoint hit');
|
console.log('Route handler: /vms endpoint hit');
|
||||||
console.log('Query params:', req.query);
|
console.log('Query params:', req.query);
|
||||||
|
|||||||
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 { DicomStudyRepository } from '../repositories/DicomStudyRepository';
|
||||||
import pool from '../config/db';
|
import pool from '../config/db';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
|
import { Pool } from 'mysql2/promise';
|
||||||
|
|
||||||
export class DicomStudyService {
|
export class DicomStudyService {
|
||||||
private repository: DicomStudyRepository;
|
private repository: DicomStudyRepository;
|
||||||
|
|
||||||
constructor() {
|
constructor(pool: Pool) {
|
||||||
this.repository = new DicomStudyRepository();
|
this.repository = new DicomStudyRepository(pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async isValidStatusCode(statusCode: string): Promise<boolean> {
|
private async isValidStatusCode(statusCode: string): Promise<boolean> {
|
||||||
@ -39,21 +40,18 @@ export class DicomStudyService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const field of requiredFields) {
|
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}`);
|
throw new Error(`Missing required field: ${field}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commented below, currently this field is inserted with active/idle
|
||||||
// Validate status code
|
// Validate status code
|
||||||
const isValidStatus = await this.isValidStatusCode(studyData.study_status_code);
|
//const isValidStatus = await this.isValidStatusCode(studyData.study_status_code);
|
||||||
if (!isValidStatus) {
|
//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`);
|
//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');
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Creating new study', { studyData });
|
logger.info('Creating new study', { studyData });
|
||||||
return await this.repository.create(studyData);
|
return await this.repository.create(studyData);
|
||||||
@ -151,4 +149,19 @@ export class DicomStudyService {
|
|||||||
throw new Error('Failed to search studies');
|
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
|
// src/services/RouterService.ts
|
||||||
import { RouterRepository } from '../repositories/RouterRepository';
|
import { RouterRepository } from '../repositories/RouterRepository';
|
||||||
import { RouterData,VMUpdate} from '../types';
|
import { Container, RouterData,VMUpdate} from '../types';
|
||||||
import { Pool } from 'mysql2/promise';
|
import { Pool } from 'mysql2/promise';
|
||||||
|
import logger from '../utils/logger';
|
||||||
|
|
||||||
|
|
||||||
export class RouterService {
|
export class RouterService {
|
||||||
@ -30,6 +31,10 @@ async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
return this.repository.findById(id);
|
return this.repository.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRouterByRouterId(routerId: string): Promise<RouterData | null> {
|
||||||
|
return this.repository.findByRouterId(routerId);
|
||||||
|
}
|
||||||
|
|
||||||
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
|
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
|
||||||
return this.repository.findByFacility(facility);
|
return this.repository.findByFacility(facility);
|
||||||
}
|
}
|
||||||
@ -45,5 +50,10 @@ async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
|
|||||||
async deleteRouter(id: number): Promise<boolean> {
|
async deleteRouter(id: number): Promise<boolean> {
|
||||||
return this.repository.delete(id);
|
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 './RouterService';
|
||||||
export * from './DicomStudyService';
|
export * from './DicomStudyService';
|
||||||
|
export * from './UtilityService';
|
||||||
|
export * from './AuthService';
|
||||||
|
export * from './UserService';
|
||||||
|
export * from './CommonService';
|
||||||
|
export * from './SetupService';
|
||||||
// Add more service exports as needed
|
// Add more service exports as needed
|
||||||
|
|||||||
@ -72,3 +72,18 @@ export interface DicomStudySearchParams {
|
|||||||
modality?: string;
|
modality?: string;
|
||||||
patientName?: 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;
|
||||||
|
}
|
||||||
@ -5,6 +5,9 @@ export interface RouterData {
|
|||||||
routerId: string; // maps to backend 'router_id'
|
routerId: string; // maps to backend 'router_id'
|
||||||
facility: string; // maps to backend 'facility'
|
facility: string; // maps to backend 'facility'
|
||||||
routerAlias: string; // maps to backend 'router_alias'
|
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'
|
lastSeen: string; // maps to backend 'last_seen'
|
||||||
diskStatus: string; // maps to backend 'disk_status_code'
|
diskStatus: string; // maps to backend 'disk_status_code'
|
||||||
diskUsage: number; // maps to backend 'disk_usage'
|
diskUsage: number; // maps to backend 'disk_usage'
|
||||||
@ -15,19 +18,27 @@ export interface RouterData {
|
|||||||
};
|
};
|
||||||
systemStatus: {
|
systemStatus: {
|
||||||
vpnStatus: string; // maps to backend 'vpn_status_code'
|
vpnStatus: string; // maps to backend 'vpn_status_code'
|
||||||
appStatus: string; // maps to backend 'disk_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[];
|
vms: VM[];
|
||||||
|
containers: Container[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Study {
|
export interface Study {
|
||||||
siuid: string;
|
|
||||||
patientId: string;
|
patientId: string;
|
||||||
accessionNumber: string;
|
|
||||||
patientName: string;
|
patientName: string;
|
||||||
|
siuid: string;
|
||||||
|
accessionNumber: string;
|
||||||
studyDate: string;
|
studyDate: string;
|
||||||
modality: string;
|
modality: string;
|
||||||
studyDescription: string;
|
studyDescription: string;
|
||||||
|
seriesInstanceUid: string;
|
||||||
|
procedureCode: string;
|
||||||
|
referringPhysicianName: string;
|
||||||
|
associationId: string;
|
||||||
|
studyStatusCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VM {
|
export interface VM {
|
||||||
@ -44,3 +55,8 @@ export interface VMUpdate {
|
|||||||
export interface VMUpdateRequest {
|
export interface VMUpdateRequest {
|
||||||
vms: VMUpdate[];
|
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