commit dc39677783484cf4be547c92e04394df15818c39 Author: rohitgir-879 Date: Thu Jun 12 00:19:44 2025 +0530 v1.0.0-rc diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c796568 Binary files /dev/null and b/.DS_Store differ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d50ac15 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +NODE_ENV = development +DB_HOST=localhost +DB_USER=root +DB_PASSWORD=Admin@123 +DB_NAME=spurrintest + +EMAIL_HOST="smtp.zoho.com" +SENDER_PORT = 465 +SENDER_SECURITY = true +EMAIL_USER="kavya.j@tech4biz.io" +EMAIL_PASS="8pQfkBw8gbrz" + +JWT_ACCESS_TOKEN_SECRET=jN4!pY9*d#T2@x$L7wq&Z8^gFc%X5@K#m +JWT_REFRESH_TOKEN_SECRET=Lx$Z7#T2^d&n9!Y4%K8@Fcg*m#qX5p@wL +JWT_ACCESS_TOKEN_EXPIRY=5h +JWT_REFRESH_TOKEN_EXPIRY=7d + +# BACK_URL = https://backend.spurrinai.com/ +BACK_URL = http://localhost:3000/ +DOMAIN_url = http://localhost:3000/ +FLASK_BASE_URL = http://localhost:5000/ + +# PORT +PORT = 3000 + +# zoho mail config for development mode + +SSL_CERT = "/home/ubuntu/spurrin-cleaned-node/certificates/fullchain.pem" +SSL_KEY = "/home/ubuntu/spurrinai-backend-node/certificates/privkey.pem" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76e9b59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/node_modules +/.env +/hospital_data +/logs +/error.log +/uploads +/llm-uploads +/certificates \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..450a65b --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,229 @@ +# Changes Log + +## [Unreleased] + +### Added +- Created comprehensive README.md with project documentation +- Implemented structured error handling system +- Added validation middleware using Joi +- Created standardized response handlers +- Implemented async handler utility +- Added custom error classes +- Created hospital validation schemas +- Updated hospital routes with proper middleware +- Added role-based authorization +- Implemented request validation +- Added structured logging +- Created separate authorization middleware with role-based access control +- Created request validation middleware with Joi schema validation +- Added repository layer for database operations +- Implemented database connection pooling +- Added custom error classes for better error handling +- Improved error handling in service layer + +### Changed +- Reorganized project structure into src directory +- Updated hospital controller to use new utilities +- Improved error handling in hospital routes +- Enhanced security with proper authentication +- Standardized API response format +- Improved code organization and readability +- Separated authentication and authorization middleware +- Enhanced validation middleware with better error handling and logging +- Refactored hospital routes for better middleware usage +- Moved logo upload logic to controller +- Updated hospital controller methods to use asyncHandler and standardized responses +- Standardized authentication and authorization across all hospital routes +- Improved error handling in hospital user and color management +- Refactored changePassword method to use asyncHandler and standardized responses +- Reordered hospital routes to prevent conflicts +- Fixed route parameter conflicts +- Moved database operations to repository layer +- Improved error handling with custom error classes +- Enhanced database connection management with connection pooling + +### Removed +- Removed unused model file (superAdminModel.js) +- Cleaned up empty directories +- Removed redundant code +- Removed inline route handlers in favor of controller methods +- Removed duplicate hospital list method +- Removed old authentication middleware usage +- Removed redundant token validation in changePassword method +- Removed unused imports from hospital routes +- Removed direct database queries from service layer + +### Fixed +- Fixed error handling in hospital controller +- Improved validation error messages +- Enhanced security in authentication flow +- Fixed response format consistency +- Fixed asyncHandler import and usage in hospital controller +- Fixed authorize function import and usage in hospital routes +- Fixed validateRequest middleware implementation +- Fixed validateRequest import in hospital routes +- Fixed missing getAllHospitals method in hospital controller +- Fixed error handling in hospital controller methods +- Fixed inconsistent authentication middleware usage +- Fixed missing controller methods and their implementations +- Fixed undefined route handler in changePassword endpoint +- Fixed route conflicts between /users and /:id endpoints +- Fixed missing changePassword route +- Fixed route ordering to prevent parameter conflicts +- Fixed database connection handling +- Fixed error propagation in service layer + +## [0.1.0] - Initial Setup + +### Added +- Basic project structure +- Database configuration +- Authentication middleware +- Hospital management endpoints +- File upload functionality +- Email notification system +- User management system +- Password reset functionality +- Interaction logging system + +### Security +- Implemented JWT authentication +- Added password hashing +- Implemented role-based access control +- Added input validation +- Implemented secure file uploads +- Added email verification system + +### Performance +- Implemented database connection pooling +- Added request compression +- Optimized database queries +- Implemented caching where appropriate + +### Documentation +- Added API documentation +- Created setup instructions +- Added security guidelines +- Included contribution guidelines + +## Hospital Module Improvements + +### Code Structure and Organization +- [x] Created dedicated `HospitalService` class for business logic +- [x] Separated concerns between routes, controller, and service layers +- [x] Improved error handling and validation +- [x] Removed duplicate code +- [x] Added proper input validation +- [x] Organized routes with proper middleware +- [x] Added repository layer for database operations +- [x] Implemented database connection pooling +- [x] Added custom error classes + +### Security Enhancements +- [x] Added rate limiting (100 requests per 15 minutes per IP) +- [x] Improved file upload security + - Added file type validation (JPEG, PNG, GIF) + - Set file size limit (5MB) + - Secure file naming +- [x] Added input validation through schemas +- [x] Enhanced error messages +- [x] Implemented proper authentication middleware +- [x] Added authorization checks + +### Database Optimization +- [x] Improved query structure +- [x] Added proper error handling for database operations +- [x] Implemented better transaction handling +- [x] Added validation before database operations +- [x] Improved error messages for database operations +- [x] Implemented connection pooling +- [x] Added repository layer for better database abstraction + +### Additional Improvements +- [x] Better error handling and logging +- [x] Consistent response formats +- [x] Improved code readability +- [x] Better separation of concerns +- [x] Added proper validation for all inputs +- [x] Improved file upload handling +- [x] Added custom error classes +- [x] Improved error propagation + +### Pending Improvements +- [ ] Add query caching for frequently accessed data +- [ ] Add request sanitization +- [ ] Implement proper CORS configuration +- [ ] Add security headers +- [ ] Add API documentation +- [ ] Add unit tests +- [ ] Add integration tests +- [ ] Add API tests + +## File Structure Changes +``` +src/ +├── controllers/ +│ └── hospitalController.js # Simplified controller with service usage +├── services/ +│ └── hospitalService.js # Business logic layer +├── repositories/ +│ └── hospitalRepository.js # Database operations layer +├── routes/ +│ └── hospitals.js # Updated with security and validation +├── middlewares/ +│ ├── authMiddleware.js # Authentication middleware +│ ├── authorizeMiddleware.js # Authorization middleware +│ └── validateRequest.js # Request validation middleware +├── utils/ +│ └── errors.js # Custom error classes +└── validators/ + └── hospitalValidator.js # Validation schemas +``` + +## Security Improvements +1. Rate Limiting + - Added express-rate-limit + - 100 requests per 15 minutes per IP + - Custom error message for rate limit exceeded + +2. File Upload Security + - File type validation + - File size limits + - Secure file naming + - Proper error handling + +3. Input Validation + - Added validation schemas + - Proper error messages + - Type checking + - Required field validation + +4. Authentication & Authorization + - Token-based authentication + - Role-based authorization + - Proper error handling for unauthorized access + +## Performance Improvements +1. Database Operations + - Optimized queries + - Better error handling + - Transaction support + - Input validation before database operations + - Connection pooling + - Repository pattern implementation + +2. Code Organization + - Service layer for business logic + - Repository layer for database operations + - Controller for request handling + - Routes for endpoint definition + - Middleware for cross-cutting concerns + +## Next Steps +1. Implement query caching +2. Add comprehensive testing +3. Add API documentation +4. Enhance security measures +5. Add monitoring and logging + +flag added to trigger logout to both websockets (secondary and main) diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..be62b5d --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,146 @@ +pipeline { + agent any + + environment { + SSH_CREDENTIALS = 'spurrin-backend-dev' + GIT_CREDENTIALS = 'gitea-cred' + REMOTE_SERVER = 'ubuntu@160.187.166.67' + REPO_HTTPS_URL = 'https://git.tech4biz.wiki/rohit/spurrin-backend.git' + BRANCH = 'main' + + REMOTE_DIR = '/home/ubuntu/spurrin-cleaned-node' + BACKUP_UPLOADS_DIR = '/home/ubuntu/uploads_backup' + VENV_PYTHON = '../venv/bin/python' + + NODE_BIN_PATH = '/home/ubuntu/.nvm/versions/node/v22.12.0/bin' + NOTIFY_EMAIL = 'jassim.mohammed@tech4biz.io' + } + + stages { + stage('Add Remote Host Key') { + steps { + echo '🔐 Adding remote host to known_hosts...' + sshagent(credentials: [SSH_CREDENTIALS]) { + sh ''' + mkdir -p ~/.ssh + ssh-keyscan -H ${REMOTE_SERVER#*@} >> ~/.ssh/known_hosts + ''' + } + } + } + +stage('Update Repo on Remote') { + steps { + echo '🔄 Pulling latest code on remote server with conditional restore and fresh backup...' + withCredentials([usernamePassword(credentialsId: "${GIT_CREDENTIALS}", usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) { + sshagent(credentials: [SSH_CREDENTIALS]) { + sh """ + ssh ${REMOTE_SERVER} ' + set -e + + # Clean old backup folder + echo "🗑️ Removing old backups..." + rm -rf ${BACKUP_UPLOADS_DIR} + mkdir -p ${BACKUP_UPLOADS_DIR} + + echo "📦 Backing up existing data..." + if [ -d ${REMOTE_DIR}/uploads ]; then + cp -a ${REMOTE_DIR}/uploads ${BACKUP_UPLOADS_DIR}/ + fi + + if [ -d ${REMOTE_DIR}/hospital_data ]; then + cp -a ${REMOTE_DIR}/hospital_data ${BACKUP_UPLOADS_DIR}/ + fi + + if [ -f ${REMOTE_DIR}/.env ]; then + cp ${REMOTE_DIR}/.env ${BACKUP_UPLOADS_DIR}/.env + fi + + if [ -d ${REMOTE_DIR}/certificates ]; then + cp -a ${REMOTE_DIR}/certificates ${BACKUP_UPLOADS_DIR}/ + fi + + # Pull latest changes without deleting local files/folders + if [ -d ${REMOTE_DIR}/.git ]; then + echo "🔁 Repo exists. Pulling latest changes..." + cd ${REMOTE_DIR} + git stash push --include-untracked --message "temp-backup-before-pull" || true + git pull origin ${BRANCH} + git stash pop || true + else + echo "📥 Repo not found. Cloning fresh and restoring backup..." + rm -rf ${REMOTE_DIR} + git clone -b ${BRANCH} https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/rohit/spurrin-backend.git ${REMOTE_DIR} + + # Restore backups only on fresh clone... + + # Restore backup only on fresh clone + if [ -d ${BACKUP_UPLOADS_DIR}/uploads ]; then + cp -a ${BACKUP_UPLOADS_DIR}/uploads ${REMOTE_DIR}/ + fi + + if [ -d ${BACKUP_UPLOADS_DIR}/hospital_data ]; then + cp -a ${BACKUP_UPLOADS_DIR}/hospital_data ${REMOTE_DIR}/ + fi + + if [ -f ${BACKUP_UPLOADS_DIR}/.env ]; then + cp ${BACKUP_UPLOADS_DIR}/.env ${REMOTE_DIR}/.env + fi + + if [ -d ${BACKUP_UPLOADS_DIR}/certificates ]; then + cp -a ${BACKUP_UPLOADS_DIR}/certificates ${REMOTE_DIR}/ + fi + fi + ' + """ + } + } + } +} + + + + stage('Install & Start Services') { + steps { + echo '🚀 Installing and starting services...' + sshagent(credentials: [SSH_CREDENTIALS]) { + sh """ + ssh ${REMOTE_SERVER} ' + set -e + export PATH=${NODE_BIN_PATH}:\$PATH + cd ${REMOTE_DIR} + npm install --legacy-peer-deps --force + + pm2 delete web-server || true + pm2 delete convo || true + + pm2 start npm --name web-server -- start + pm2 start chat.py --interpreter ${VENV_PYTHON} --name=convo + ' + """ + } + } + } + } + + post { + always { + echo '🧹 Cleaning workspace...' + cleanWs() + } + + success { + echo '✅ Deployment successful!' + mail to: "${NOTIFY_EMAIL}", + subject: "✅ Jenkins - spurrin-cleaned-node Deployment Successful", + body: "The deployment of spurrin-cleaned-node to ${REMOTE_SERVER} was successful.\n\nRegards,\nJenkins" + } + + failure { + echo '❌ Deployment failed!' + mail to: "${NOTIFY_EMAIL}", + subject: "❌ Jenkins - spurrin-cleaned-node Deployment Failed", + body: "The deployment of spurrin-cleaned-node to ${REMOTE_SERVER} failed. Please check Jenkins logs.\n\nRegards,\nJenkins" + } + } +} \ No newline at end of file diff --git a/__pycache__/model_manager.cpython-312.pyc b/__pycache__/model_manager.cpython-312.pyc new file mode 100644 index 0000000..dcce957 Binary files /dev/null and b/__pycache__/model_manager.cpython-312.pyc differ diff --git a/chat.py b/chat.py new file mode 100644 index 0000000..8a84749 --- /dev/null +++ b/chat.py @@ -0,0 +1,2269 @@ +""" +SpurrinAI - Intelligent Document Processing and Question Answering System +Copyright (c) 2024 Tech4biz. All rights reserved. + +This module implements the main Flask application for the SpurrinAI system, +providing REST APIs for document processing, vector storage, and question answering +using RAG (Retrieval Augmented Generation) architecture. + +Author: Tech4biz Development Team +Version: 1.0.0 +Last Updated: 2024-01-19 +""" + +# Standard library imports +import os +import re +import sys +import json +import time +import threading +import asyncio +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from datetime import timedelta +from enum import Enum + +# Third-party imports +import spacy +import redis +import aiomysql +from dotenv import load_dotenv +from flask import Flask, request, jsonify, Response +from flask_cors import CORS +from tqdm import tqdm +from tqdm.asyncio import tqdm as tqdm_async +from langchain_community.document_loaders import PyPDFLoader +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_community.embeddings import OpenAIEmbeddings +from langchain_community.vectorstores import Chroma +from langchain_community.chat_models import ChatOpenAI +from langchain.chains import RetrievalQA +from langchain.prompts import PromptTemplate +from openai import OpenAI +from rapidfuzz import process +from threading import Lock + +# Local imports +from model_manager import ModelManager + +# Suppress warnings +import warnings + +warnings.filterwarnings("ignore") + +# Initialize NLTK +import nltk + +nltk.download("punkt") + +# Configure logging +import logging +import logging.handlers + +app = Flask(__name__) +CORS(app) + +script_dir = os.path.dirname(os.path.abspath(__file__)) +log_file_path = os.path.join(script_dir, "error.log") +logging.basicConfig(filename=log_file_path, level=logging.INFO) + + +# Configure logging +def setup_logging(): + log_dir = os.path.join(script_dir, "logs") + os.makedirs(log_dir, exist_ok=True) + + main_log = os.path.join(log_dir, "app.log") + error_log = os.path.join(log_dir, "error.log") + access_log = os.path.join(log_dir, "access.log") + perf_log = os.path.join(log_dir, "performance.log") + + # Create formatters + detailed_formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s" + ) + access_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + + # Main logger setup + main_handler = logging.handlers.RotatingFileHandler( + main_log, maxBytes=10485760, backupCount=5 + ) + main_handler.setFormatter(detailed_formatter) + main_handler.setLevel(logging.INFO) + + # Error logger setup + error_handler = logging.handlers.RotatingFileHandler( + error_log, maxBytes=10485760, backupCount=5 + ) + error_handler.setFormatter(detailed_formatter) + error_handler.setLevel(logging.ERROR) + + # Access logger setup + access_handler = logging.handlers.TimedRotatingFileHandler( + access_log, when="midnight", interval=1, backupCount=30 + ) + access_handler.setFormatter(access_formatter) + access_handler.setLevel(logging.INFO) + + # Performance logger setup + perf_handler = logging.handlers.RotatingFileHandler( + perf_log, maxBytes=10485760, backupCount=5 + ) + perf_handler.setFormatter(detailed_formatter) + perf_handler.setLevel(logging.INFO) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + root_logger.addHandler(main_handler) + root_logger.addHandler(error_handler) + + # Create specific loggers + access_logger = logging.getLogger("access") + access_logger.addHandler(access_handler) + access_logger.setLevel(logging.INFO) + + perf_logger = logging.getLogger("performance") + perf_logger.addHandler(perf_handler) + perf_logger.setLevel(logging.INFO) + + return root_logger, access_logger, perf_logger + +load_dotenv() +# Initialize loggers +logger, access_logger, perf_logger = setup_logging() + +# DB_CONFIG = { +# 'host': 'localhost', +# 'user': 'flaskuser', +# 'password': 'Flask@123', +# 'database': 'spurrinai', +# } + +# DB_CONFIG = { +# 'host': 'localhost', +# 'user': 'spurrindevuser', +# 'password': 'Admin@123', +# 'database': 'spurrindev', +# } + +# Redis Configuration +REDIS_CONFIG = { + "host": "localhost", + "port": 6379, + "db": 0, + "decode_responses": True, # For string operations +} + +DB_CONFIG = { + "host": os.getenv("DB_HOST"), + "user": os.getenv("DB_USER"), + "password": os.getenv("DB_PASSWORD"), + "database": os.getenv("DB_NAME"), +} + +# Redis connection pool +redis_pool = redis.ConnectionPool(**REDIS_CONFIG) +redis_binary_pool = redis.ConnectionPool( + host="localhost", port=6379, db=1, decode_responses=False +) + + +def get_redis_client(binary=False): + """Get Redis client from pool""" + logger.debug(f"Getting Redis client with binary={binary}") + try: + pool = redis_binary_pool if binary else redis_pool + client = redis.Redis(connection_pool=pool) + logger.debug("Redis client created successfully") + return client + except Exception as e: + logger.error(f"Failed to create Redis client: {e}", exc_info=True) + raise + + +def fetch_cached_answer(cache_key): + logger.debug(f"Attempting to fetch cached answer for key: {cache_key}") + start_time = time.time() + try: + redis_client = get_redis_client() + cached_answer = redis_client.get(cache_key) + fetch_time = time.time() - start_time + perf_logger.info( + f"Redis fetch completed in {fetch_time:.3f} seconds for key: {cache_key}" + ) + return cached_answer + except Exception as e: + logger.error(f"Redis fetch error for key {cache_key}: {e}", exc_info=True) + return None + + +# Cache TTL configurations +CACHE_TTL = { + "vector_store": timedelta(hours=24), + "chat_completion": timedelta(hours=1), + "document_metadata": timedelta(days=7), +} + +DATA_DIR = os.path.join(script_dir, "hospital_data") +CHROMA_DIR = os.path.join(DATA_DIR, "chroma_db") +uploads_dir = os.path.join(script_dir, "llm-uploads") + +if not os.path.exists(uploads_dir): + os.makedirs(uploads_dir) + +nlp = spacy.load("en_core_web_sm") + +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + +client = OpenAI(api_key=OPENAI_API_KEY) +embeddings = OpenAIEmbeddings(api_key=OPENAI_API_KEY) +llm = ChatOpenAI( + model_name="gpt-3.5-turbo", streaming=True, temperature=0.2, api_key=OPENAI_API_KEY +) +# text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50) +hospital_vector_stores = {} +vector_store_lock = threading.Lock() + + +@dataclass +class Document: + doc_id: int + page_num: int + content: str + + +class DocumentStatus(Enum): + PROCESSING = "processing" + PROCESSED = "processed" + FAILED = "failed" + + +async def get_db_pool(): + return await aiomysql.create_pool( + host=DB_CONFIG["host"], + user=DB_CONFIG["user"], + password=DB_CONFIG["password"], + db=DB_CONFIG["database"], + autocommit=True, + ) + + +async def get_hospital_id(hospital_code): + try: + pool = await get_db_pool() + async with pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute( + "SELECT id FROM hospitals WHERE hospital_code = %s LIMIT 1", + (hospital_code,), + ) + result = await cursor.fetchone() + return result["id"] if result else None + except Exception as error: + logging.error(f"Database error: {error}") + return None + finally: + pool.close() + await pool.wait_closed() + + +CHUNK_SIZE = 4000 +CHUNK_OVERLAP = 150 +BATCH_SIZE = 1000 + +text_splitter = RecursiveCharacterTextSplitter( + chunk_size=CHUNK_SIZE, + chunk_overlap=CHUNK_OVERLAP, + # length_function=len, + # separators=["\n\n", "\n", ". ", " ", ""] +) + + +# Update the JSON_PATH to be dynamic based on hospital_id +def get_icd_json_path(hospital_id): + hospital_data_dir = os.path.join(DATA_DIR, f"hospital_{hospital_id}") + os.makedirs(hospital_data_dir, exist_ok=True) + return os.path.join(hospital_data_dir, "icd_data.json") + + +def extract_and_process_icd_data(content, hospital_id, save_to_json=True): + """Extract and process ICD codes with optimized processing and optional JSON saving""" + try: + # Initialize pattern compilation once + pattern = re.compile(r"^\s*([A-Z][0-9A-Z]{2,6}[A-Z]?)\s+(.*)$", re.MULTILINE) + + # Process in chunks for large content + chunk_size = 50000 # Process 50KB at a time + icd_data = [] + + current_code = None + current_description = [] + + # Split content into manageable chunks + content_chunks = [ + content[i : i + chunk_size] for i in range(0, len(content), chunk_size) + ] + + # Process each chunk + for chunk in content_chunks: + lines = chunk.splitlines() + + for line in lines: + line = line.strip() + if not line: + if current_code and current_description: + icd_data.append( + { + "code": current_code, + "description": " ".join(current_description).strip(), + } + ) + current_code = None + current_description = [] + continue + + match = pattern.match(line) + if match: + if current_code and current_description: + icd_data.append( + { + "code": current_code, + "description": " ".join(current_description).strip(), + } + ) + current_code, description = match.groups() + current_description = [description.strip()] + elif current_code: + current_description.append(line) + + # Add final entry if exists + if current_code and current_description: + icd_data.append( + { + "code": current_code, + "description": " ".join(current_description).strip(), + } + ) + + # Save to hospital-specific JSON if requested + if save_to_json and icd_data: + try: + json_path = get_icd_json_path(hospital_id) + + # Use a lock for thread safety + with threading.Lock(): + if os.path.exists(json_path): + with open(json_path, "r", encoding="utf-8") as f: + try: + existing_data = json.load(f) + except json.JSONDecodeError: + existing_data = [] + else: + existing_data = [] + + # Efficient deduplication using dictionary + seen_codes = {item["code"]: item for item in existing_data} + for item in icd_data: + seen_codes[item["code"]] = item + + unique_data = list(seen_codes.values()) + + # Write atomically using temporary file + temp_path = f"{json_path}.tmp" + with open(temp_path, "w", encoding="utf-8") as f: + json.dump(unique_data, f, indent=2, ensure_ascii=False) + os.replace(temp_path, json_path) + + logging.info( + f"Successfully saved {len(unique_data)} unique ICD codes to JSON for hospital {hospital_id}" + ) + + except Exception as e: + logging.error( + f"Error saving ICD data to JSON for hospital {hospital_id}: {e}" + ) + + return icd_data + + except Exception as e: + logging.error(f"Error in extract_and_process_icd_data: {e}") + return [] + + +def load_icd_entries(hospital_id): + """Load ICD entries from hospital-specific JSON file""" + json_path = get_icd_json_path(hospital_id) + try: + if os.path.exists(json_path): + with open(json_path, "r", encoding="utf-8") as f: + return json.load(f) + return [] + except Exception as e: + logging.error(f"Error loading ICD entries for hospital {hospital_id}: {e}") + return [] + + +# Update the process_icd_codes function to include hospital_id +async def process_icd_codes(content, doc_id, hospital_id, batch_size=256): + """Process and store ICD codes using the optimized extraction function""" + try: + # Extract and save codes with hospital_id + extract_and_process_icd_data(content, hospital_id, save_to_json=True) + except Exception as e: + logging.error(f"Error processing ICD codes for hospital {hospital_id}: {e}") + + +async def initialize_icd_vector_store(hospital_id): + """This function is deprecated. ICD codes are now handled through JSON search.""" + logging.warning( + "initialize_icd_vector_store is deprecated - using JSON search instead" + ) + return None + + +def extract_pdf_contents(pdf_path, hospital_id): + """Extract PDF contents with optimized chunking and code extraction""" + try: + loader = PyPDFLoader(pdf_path) + pages = loader.load() + pages_content = [] + + for i, page in enumerate(tqdm(pages, desc="Extracting pages")): + text = page.page_content.strip() + + # Extract ICD codes from the page + icd_codes = extract_and_process_icd_data( + text, hospital_id + ) # We'll set doc_id later + + pages_content.append({"page": i + 1, "text": text, "codes": icd_codes}) + + return pages_content + + except Exception as e: + logging.error(f"Error in extract_pdf_contents: {e}") + raise + + +async def insert_content_into_db(content, metadata, doc_id): + pool = await get_db_pool() + async with pool.acquire() as conn: + async with conn.cursor() as cursor: + try: + metadata_query = "INSERT INTO document_metadata (document_id, key_name, value_name) VALUES (%s, %s, %s)" + content_query = "INSERT INTO document_pages (document_id, page_number, content) VALUES (%s, %s, %s)" + + metadata_values = [ + (doc_id, key[:100], value) + for key, value in metadata.items() + if value + ] + content_values = [ + (doc_id, page_content["page"], page_content["text"]) + for page_content in content + ] + + if metadata_values: + await cursor.executemany(metadata_query, metadata_values) + if content_values: + await cursor.executemany(content_query, content_values) + + await conn.commit() + return {"message": "Success"} + except Exception as e: + await conn.rollback() + return {"error": str(e)} + + +async def initialize_or_load_vector_store(hospital_id, user_id="default"): + """Initialize or load vector store with Redis caching and thread safety""" + store_key = f"{hospital_id}:{user_id}" + + try: + # Check if we already have it loaded - with lock for thread safety + with vector_store_lock: + if store_key in hospital_vector_stores: + return hospital_vector_stores[store_key] + + # Initialize vector store + redis_client = get_redis_client(binary=True) + cache_key = f"vector_store_data:{hospital_id}:{user_id}" + hospital_dir = os.path.join(CHROMA_DIR, f"hospital_{hospital_id}") + + if os.path.exists(hospital_dir): + logging.info( + f"Loading vector store for hospital {hospital_id} and user {user_id}" + ) + vector_store = await asyncio.to_thread( + lambda: Chroma( + collection_name=f"hospital_{hospital_id}", + persist_directory=hospital_dir, + embedding_function=embeddings, + ) + ) + else: + logging.info(f"Creating vector store for hospital {hospital_id}") + os.makedirs(hospital_dir, exist_ok=True) + vector_store = await asyncio.to_thread( + lambda: Chroma( + collection_name=f"hospital_{hospital_id}", + persist_directory=hospital_dir, + embedding_function=embeddings, + ) + ) + + # Store with lock for thread safety + with vector_store_lock: + hospital_vector_stores[store_key] = vector_store + + return vector_store + except Exception as e: + logging.error(f"Error initializing vector store: {e}", exc_info=True) + raise + + +async def delete_document_vectors(hospital_id: int, doc_id: str) -> bool: + """Delete all vectors associated with a specific document from ChromaDB""" + try: + # Initialize vector store for the hospital + vector_store = await initialize_or_load_vector_store(hospital_id) + + # Delete vectors with matching doc_id + await asyncio.to_thread( + lambda: vector_store._collection.delete(where={"doc_id": str(doc_id)}) + ) + + # Persist changes + await asyncio.to_thread(vector_store.persist) + + # Clear Redis cache for this document + redis_client = get_redis_client() + pattern = f"vector_store_data:{hospital_id}:*" + for key in redis_client.scan_iter(pattern): + redis_client.delete(key) + + logging.info( + f"Successfully deleted vectors for document {doc_id} from hospital {hospital_id}" + ) + return True + + except Exception as e: + logging.error(f"Error deleting document vectors: {e}", exc_info=True) + return False + + +async def add_document_to_index(doc_id, hospital_id): + try: + pool = await get_db_pool() + async with pool.acquire() as conn: + async with conn.cursor() as cursor: + vector_store = await initialize_or_load_vector_store(hospital_id) + + await cursor.execute( + "SELECT page_number, content FROM document_pages WHERE document_id = %s ORDER BY page_number", + (doc_id,), + ) + rows = await cursor.fetchall() + + total_pages = len(rows) + logging.info(f"Processing {total_pages} pages for document {doc_id}") + page_bar = tqdm_async(total=total_pages, desc="Processing pages") + + async def process_page(page_data): + page_num, content = page_data + try: + icd_data = extract_and_process_icd_data( + content, hospital_id, save_to_json=False + ) + chunks = text_splitter.split_text(content) + await asyncio.sleep(0) # Yield control + return page_num, chunks, icd_data + except Exception as e: + logging.error(f"Error processing page {page_num}: {e}") + return page_num, [], [] + + tasks = [asyncio.create_task(process_page(row)) for row in rows] + results = [] + + for coro in asyncio.as_completed(tasks): + result = await coro + results.append(result) + page_bar.update(1) + + page_bar.close() + + # Vector addition progress bar + all_icd_data = [] + all_chunks = [] + all_metadatas = [] + + chunk_add_bar = tqdm_async(desc="Vectorizing chunks", total=0) + + for result in results: + page_num, chunks, icd_data = result + all_icd_data.extend(icd_data) + + for i, chunk in enumerate(chunks): + all_chunks.append(chunk) + all_metadatas.append( + { + "doc_id": str(doc_id), + "hospital_id": str(hospital_id), + "page_number": str(page_num), + "chunk_index": str(i), + } + ) + + if len(all_chunks) >= BATCH_SIZE: + chunk_add_bar.total += len(all_chunks) + chunk_add_bar.refresh() + await asyncio.to_thread( + vector_store.add_texts, + texts=all_chunks, + metadatas=all_metadatas, + ) + all_chunks = [] + all_metadatas = [] + chunk_add_bar.update(BATCH_SIZE) + + # Final batch + if all_chunks: + chunk_add_bar.total += len(all_chunks) + chunk_add_bar.refresh() + await asyncio.to_thread( + vector_store.add_texts, + texts=all_chunks, + metadatas=all_metadatas, + ) + chunk_add_bar.update(len(all_chunks)) + + chunk_add_bar.close() + + if all_icd_data: + logging.info(f"Saving {len(all_icd_data)} ICD codes") + extract_and_process_icd_data("", hospital_id, save_to_json=True) + + await asyncio.to_thread(vector_store.persist) + logging.info(f"Successfully indexed document {doc_id}") + return True + + except Exception as e: + logging.error(f"Error adding document: {e}") + return False + + +def is_general_knowledge_question( + query: str, context: str, conversation_context=None +) -> bool: + """ + Determine if a question is likely a general knowledge question not covered in the documents. + Takes conversation history into account to reduce repeated confirmations. + """ + query_lower = query.lower() + context_lower = context.lower() + + if conversation_context: + for interaction in conversation_context: + prev_question = interaction.get("question", "").lower() + if ( + prev_question + and query_lower in prev_question + or prev_question in query_lower + ): + logging.info( + f"Question is similar to previous conversation, skipping confirmation" + ) + return False + + stop_words = { + "search", + "query:", + "can", + "you", + "some", + "at", + "the", + "a", + "an", + "in", + "on", + "at", + "to", + "for", + "with", + "by", + "about", + "give", + "full", + "is", + "are", + "was", + "were", + "define", + "what", + "how", + "why", + "when", + "where", + "year", + "list", + "form", + "table", + "who", + "which", + "me", + "tell", + "explain", + "describe", + "of", + "and", + "or", + "there", + "their", + "please", + "could", + "would", + "various", + "different", + "type", + "types", + "kind", + "kinds", + "has", + "have", + "had", + "many", + "say", + } + + key_words = [ + word for word in query_lower.split() if word not in stop_words and len(word) > 2 + ] + logging.info(f"Key words: {key_words}") + + if not key_words: + logging.info("No significant keywords found, directing to general knowledge") + return True + + matches = sum(1 for word in key_words if word in context_lower) + logging.info(f"Matches: {matches} out of {len(key_words)} keywords") + + match_ratio = matches / len(key_words) + logging.info(f"Match ratio: {match_ratio}") + + return match_ratio < 0.6 + + +def is_table_request(query: str) -> bool: + """ + Determine if the user is requesting a response in tabular format. + """ + table_keywords = [ + "table", + "tabular", + "in a table", + "in table format", + "in tabular format", + "chart", + "data", + "comparison", + "as a table", + "table format", + "in rows and columns", + "in a grid", + "breakdown", + "spreadsheet", + "comparison table", + "data table", + "structured table", + "tabular form", + "table form", + ] + + query_lower = query.lower() + return any(keyword in query_lower for keyword in table_keywords) + + +import re + + +def ensure_html_response(text: str) -> str: + """ + Ensure the response is properly formatted in HTML. + This function handles plain text conversion to HTML. + """ + if "", text)) + + if not has_html_tags: + paragraphs = text.split("\n\n") + html_parts = [] + in_ordered_list = False + in_unordered_list = False + + for para in paragraphs: + if para.strip(): + if re.match(r"^\s*[\*\-\•]\s", para): + if not in_unordered_list: + html_parts.append("") + in_unordered_list = False + + html_parts.append(f"

{para}

") + + if in_ordered_list: + html_parts.append("") + if in_unordered_list: + html_parts.append("") + + return "".join(html_parts) + + else: + if not any(tag in text for tag in ("

", "

", "
    ", "
      ")): + paragraphs = text.split("\n\n") + html_parts = [f"

      {para}

      " for para in paragraphs if para.strip()] + return "".join(html_parts) + + return text + + +class HybridConversationManager: + """ + Hybrid conversation manager that uses Redis for RAG-based conversations + and in-memory storage for general knowledge conversations. + """ + + def __init__(self, redis_client, ttl=3600, max_history_items=5): + self.redis_client = redis_client + self.ttl = ttl + self.max_history_items = max_history_items + + # For general knowledge questions (in-memory) + self.general_knowledge_histories = {} + self.lock = Lock() + + def _get_redis_key(self, user_id, hospital_id, session_id=None): + """Create Redis key for document-based conversations.""" + if session_id: + return f"conv_history:{user_id}:{hospital_id}:{session_id}" + return f"conv_history:{user_id}:{hospital_id}" + + def _get_memory_key(self, user_id, hospital_id, session_id=None): + """Create memory key for general knowledge conversations.""" + if session_id: + return f"{user_id}:{hospital_id}:{session_id}" + return f"{user_id}:{hospital_id}" + + async def add_rag_interaction( + self, user_id, hospital_id, question, answer, session_id=None + ): + """Add document-based (RAG) interaction to Redis.""" + key = self._get_redis_key(user_id, hospital_id, session_id) + history = self.get_rag_history(user_id, hospital_id, session_id) + + # Add new interaction + history.append( + { + "question": question, + "answer": answer, + "timestamp": time.time(), + "type": "rag", # Mark as RAG-based interaction + } + ) + + # Keep only last N interactions + history = history[-self.max_history_items :] + + # Store updated history + try: + self.redis_client.setex(key, self.ttl, json.dumps(history)) + logging.info( + f"Stored RAG interaction in Redis for {user_id}:{hospital_id}:{session_id}" + ) + except Exception as e: + logging.error(f"Failed to store RAG interaction in Redis: {e}") + + def add_general_knowledge_interaction( + self, user_id, hospital_id, question, answer, session_id=None + ): + """Add general knowledge interaction to in-memory store.""" + key = self._get_memory_key(user_id, hospital_id, session_id) + + with self.lock: + if key not in self.general_knowledge_histories: + self.general_knowledge_histories[key] = [] + + self.general_knowledge_histories[key].append( + { + "question": question, + "answer": answer, + "timestamp": time.time(), + "type": "general", # Mark as general knowledge interaction + } + ) + + # Keep only the most recent interactions + if len(self.general_knowledge_histories[key]) > self.max_history_items: + self.general_knowledge_histories[key] = ( + self.general_knowledge_histories[key][-self.max_history_items :] + ) + + logging.info( + f"Stored general knowledge interaction in memory for {user_id}:{hospital_id}:{session_id}" + ) + + def get_rag_history(self, user_id, hospital_id, session_id=None): + """Get document-based (RAG) conversation history from Redis.""" + key = self._get_redis_key(user_id, hospital_id, session_id) + try: + history_data = self.redis_client.get(key) + return json.loads(history_data) if history_data else [] + except Exception as e: + logging.error(f"Failed to retrieve RAG history from Redis: {e}") + return [] + + def get_general_knowledge_history(self, user_id, hospital_id, session_id=None): + """Get general knowledge conversation history from memory.""" + key = self._get_memory_key(user_id, hospital_id, session_id) + + with self.lock: + return self.general_knowledge_histories.get(key, []).copy() + + def get_combined_history(self, user_id, hospital_id, session_id=None): + """Get combined conversation history from both sources, sorted by timestamp.""" + rag_history = self.get_rag_history(user_id, hospital_id, session_id) + general_history = self.get_general_knowledge_history( + user_id, hospital_id, session_id + ) + + # Combine histories + combined_history = rag_history + general_history + + # Sort by timestamp (newest first) + combined_history.sort(key=lambda x: x.get("timestamp", 0), reverse=True) + + # Return most recent N items + return combined_history[: self.max_history_items] + + def get_context_window(self, user_id, hospital_id, session_id=None, window_size=2): + """Get the most recent interactions for context from combined history.""" + combined_history = self.get_combined_history(user_id, hospital_id, session_id) + # Sort by timestamp (oldest first) for context window + sorted_history = sorted(combined_history, key=lambda x: x.get("timestamp", 0)) + return sorted_history[-window_size:] if sorted_history else [] + + def clear_history(self, user_id, hospital_id): + """Clear conversation history from both stores.""" + # Clear Redis history + redis_key = self._get_redis_key(user_id, hospital_id) + try: + self.redis_client.delete(redis_key) + except Exception as e: + logging.error(f"Failed to clear Redis history: {e}") + + # Clear memory history + memory_key = self._get_memory_key(user_id, hospital_id) + with self.lock: + if memory_key in self.general_knowledge_histories: + del self.general_knowledge_histories[memory_key] + + +class ContextMapper: + """Enhanced context mapping using shared model manager""" + + def __init__(self): + self.model_manager = ModelManager() + self.context_cache = {} + self.similarity_threshold = 0.6 + + def get_semantic_similarity(self, text1, text2): + """Get semantic similarity using global model manager""" + return self.model_manager.get_semantic_similarity(text1, text2) + + def extract_key_concepts(self, text): + """Extract key concepts using NLP techniques""" + doc = nlp(text) + concepts = [] + + entities = [(ent.text, ent.label_) for ent in doc.ents] + noun_phrases = [chunk.text for chunk in doc.noun_chunks] + important_words = [ + token.text for token in doc if token.pos_ in ["NOUN", "PROPN", "VERB"] + ] + + concepts.extend([e[0] for e in entities]) + concepts.extend(noun_phrases) + concepts.extend(important_words) + + return list(set(concepts)) + + def map_conversation_context( + self, current_query, conversation_history, context_window=3 + ): + """Map conversation context using enhanced NLP techniques""" + if not conversation_history: + return current_query + + recent_context = conversation_history[-context_window:] + context_concepts = [] + + # Extract concepts from recent conversations + for interaction in recent_context: + q_concepts = self.extract_key_concepts(interaction["question"]) + a_concepts = self.extract_key_concepts(interaction["answer"]) + context_concepts.extend(q_concepts) + context_concepts.extend(a_concepts) + + # Extract concepts from current query + query_concepts = self.extract_key_concepts(current_query) + + # Find related concepts + related_concepts = [] + for q_concept in query_concepts: + for c_concept in context_concepts: + similarity = self.get_semantic_similarity(q_concept, c_concept) + if similarity > self.similarity_threshold: + related_concepts.append(c_concept) + + # Build enhanced query + if related_concepts: + enhanced_query = ( + f"{current_query} in context of {', '.join(related_concepts)}" + ) + else: + enhanced_query = current_query + + return enhanced_query + + +# Initialize the context mapper +context_mapper = ContextMapper() + + +async def generate_contextual_query( + question: str, user_id: str, hospital_id: int, conversation_manager +) -> str: + """Generate enhanced contextual query""" + context_window = conversation_manager.get_context_window(user_id, hospital_id) + + if not context_window: + return question + + # Enhanced context mapping + last_interaction = context_window[-1] + enhanced_context = f""" + Previous question: {last_interaction['question']} + Previous answer: {last_interaction['answer']} + Current question: {question} + + Please generate a detailed search query that combines the context from the previous answer + with the current question, especially if the current question uses words like 'it', 'this', + 'that', or asks for more details about the previous topic. + """ + + try: + response = await asyncio.to_thread( + lambda: client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + { + "role": "system", + "content": "You are a context-aware query generator.", + }, + {"role": "user", "content": enhanced_context}, + ], + temperature=0.3, + max_tokens=150, + ) + ) + contextual_query = response.choices[0].message.content.strip() + logging.info(f"Enhanced contextual query: {contextual_query}") + return contextual_query + except Exception as e: + logging.error(f"Error generating contextual query: {e}") + return question + + +def is_follow_up(current_question: str, conversation_history: list) -> bool: + """Enhanced follow-up detection using NLP techniques""" + if not conversation_history: + return False + + last_interaction = conversation_history[-1] + + # Get semantic similarity with higher threshold + similarity = context_mapper.get_semantic_similarity( + current_question, f"{last_interaction['question']} {last_interaction['answer']}" + ) + + # Enhanced referential check + doc = nlp(current_question.lower()) + has_referential = any( + token.lemma_ + in [ + "it", + "this", + "that", + "these", + "those", + "they", + "he", + "she", + "about", + "more", + ] + for token in doc + ) + + # Extract concepts with improved entity detection + current_concepts = set(context_mapper.extract_key_concepts(current_question)) + last_concepts = set( + context_mapper.extract_key_concepts( + f"{last_interaction['question']} {last_interaction['answer']}" + ) + ) + + # Calculate enhanced concept overlap + concept_overlap = ( + len(current_concepts & last_concepts) / len(current_concepts | last_concepts) + if current_concepts + else 0 + ) + + # More aggressive follow-up detection + return ( + similarity > 0.3 # Lowered threshold + or has_referential + or concept_overlap > 0.2 # Lowered threshold + or any( + word in current_question.lower() + for word in ["more", "about", "elaborate", "explain"] + ) + ) + +async def get_relevant_context(question, hospital_id, doc_id=None): + try: + cache_key = f"context:hospital_{hospital_id}" + if doc_id: + cache_key += f":doc_{doc_id}" + cache_key += f":{question.lower().strip()}" + + redis_client = get_redis_client() + cached_context = redis_client.get(cache_key) + if cached_context: + logging.info(f"Cache hit for key: {cache_key}") + return cached_context.decode("utf-8") if isinstance(cached_context, bytes) else cached_context + + vector_store = await initialize_or_load_vector_store(hospital_id) + if not vector_store: + return "" + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={ + "k": 5, # Reduced number of documents for precision + "fetch_k": 10, # Reduced fetch size + "lambda_mult": 0.8, # Increase diversity in MMR + "score_threshold": 0.7, # Add minimum similarity score + "filter": {"doc_id": str(doc_id)} if doc_id else {"hospital_id": str(hospital_id)} + }, + ) + + docs = await asyncio.to_thread(retriever.get_relevant_documents, question) + if not docs: + logging.info(f"No relevant documents found for question: {question}") + return "" + + # # Filter documents by relevance using spaCy similarity + # question_doc = nlp(question) + # relevant_docs = [] + # for doc in docs: + # doc_content = nlp(doc.page_content) + # similarity = question_doc.similarity(doc_content) + # if similarity >= 0.7: # Strict similarity threshold + # relevant_docs.append(doc) + + # if not relevant_docs: + # logging.info({relevant_docs}) + # logging.info(f"No sufficiently relevant documents after similarity filtering for: {question}") + # return "" + + sorted_docs = sorted( + docs, + key=lambda x: ( + int(x.metadata.get("page_number", 0)), + int(x.metadata.get("chunk_index", 0)), + ), + ) + + context_parts = [doc.page_content for doc in sorted_docs] + context = "\n\n".join(context_parts) + + try: + redis_client.setex( + cache_key, + int(CACHE_TTL["vector_store"].total_seconds()), + context.encode("utf-8") if isinstance(context, str) else context, + ) + logging.info(f"Cached context for key: {cache_key}") + except Exception as cache_error: + logging.error(f"Failed to cache context: {cache_error}") + + return context + except Exception as e: + logging.error(f"Error getting relevant context: {e}") + return "" + +def format_conversation_context(conv_history): + """Format conversation history into a string""" + if not conv_history: + return "No previous conversation." + return "\n".join( + [ + f"Q: {interaction['question']}\nA: {interaction['answer']}" + for interaction in conv_history + ] + ) + + +def get_icd_context_from_question(question, hospital_id): + """Extract any valid ICD codes from the question and return context""" + icd_data = load_icd_entries(hospital_id) + matches = [] + code_pattern = re.findall(r"\b([A-Z][0-9A-Z]{2,6}[A-Z]?)\b", question.upper()) + + seen = set() + for code in code_pattern: + for entry in icd_data: + if entry["code"] == code and code not in seen: + matches.append(f"{entry['code']}: {entry['description']}") + seen.add(code) + return "\n".join(matches) + + +def get_fuzzy_icd_context(question, hospital_id, top_n=5, threshold=70): + """Get fuzzy matches for ICD codes from the question""" + icd_data = load_icd_entries(hospital_id) + descriptions = [entry["description"] for entry in icd_data] + matches = process.extract( + question, descriptions, limit=top_n, score_cutoff=threshold + ) + + matched_context = [] + for desc, score, _ in matches: + for entry in icd_data: + if entry["description"] == desc: + matched_context.append(f"{entry['code']}: {entry['description']}") + break + + return "\n".join(matched_context) + + +# async def generate_answer_with_rag( +# question, +# hospital_id, +# client, +# doc_id=None, +# user_id="default", +# conversation_manager=None, +# session_id=None, +# ): +# """Generate an answer using RAG with improved conversation flow""" +# try: +# # Continue with regular RAG processing if not an ICD code or if no ICD match found +# html_instruction = """ +# IMPORTANT: Format your ENTIRE response as HTML. Use appropriate HTML tags for all content: +# - Use

      tags for paragraphs +# - Use

      ,

      tags for headings and subheadings +# - Use
        ,
      • tags for bullet points +# - Use
          ,
        1. tags for numbered lists +# - Use
          for quoted text +# - Use for bold text and for emphasis +# """ + +# table_instruction = """ +# - For tables, use proper HTML table structure: +# +# +# +# +# +# +# {table_headers} +# +# +# +# {table_rows} +# +#
          {table_title}
          +# """ +# # Get conversation history first +# conv_history = ( +# conversation_manager.get_context_window(user_id, hospital_id, session_id) +# if conversation_manager +# else [] +# ) + +# # Get contextual query and relevant context first +# contextual_query = await generate_contextual_query( +# question, user_id, hospital_id, conversation_manager +# ) +# # Track ICD context across conversation +# icd_context = {} +# if conv_history: +# # Extract ICD code from previous interaction +# last_answer = conv_history[-1].get("answer", "") +# icd_codes = re.findall(r"\b([A-Z][0-9A-Z]{2,6}[A-Z]?)\b", last_answer) +# if icd_codes: +# icd_context["last_code"] = icd_codes[0] + +# # Check if current question is about a previously discussed ICD code +# is_icd_followup = False +# if icd_context.get("last_code"): +# followup_indicators = [ +# "what causes", +# "what is causing", +# "why", +# "how", +# "symptoms", +# "treatment", +# "diagnosis", +# "causes", +# "effects", +# "complications", +# "risk factors", +# "prevention", +# "prognosis", +# "this", +# "disease", +# "that", +# "it", +# ] +# is_icd_followup = any( +# indicator in question.lower() for indicator in followup_indicators +# ) + +# if is_icd_followup: +# # Add the previous ICD code context to the current question +# icd_exact_context = get_icd_context_from_question( +# icd_context["last_code"], hospital_id +# ) +# icd_fuzzy_context = get_fuzzy_icd_context( +# f"{icd_context['last_code']} {question}", hospital_id +# ) +# else: +# icd_exact_context = get_icd_context_from_question(question, hospital_id) +# icd_fuzzy_context = get_fuzzy_icd_context(question, hospital_id) +# else: +# icd_exact_context = get_icd_context_from_question(question, hospital_id) +# icd_fuzzy_context = get_fuzzy_icd_context(question, hospital_id) + +# # Get contextual query and relevant context +# contextual_query = await generate_contextual_query( +# question, user_id, hospital_id, conversation_manager +# ) +# doc_context = await get_relevant_context(contextual_query, hospital_id, doc_id) + +# # Combine context with priority for ICD information +# context_parts = [] +# if is_icd_followup: +# context_parts.append( +# f"## Previous ICD Code Context\nContinuing discussion about: {icd_context['last_code']}" +# ) +# if icd_exact_context: +# context_parts.append("## ICD Code Match\n" + icd_exact_context) +# if icd_fuzzy_context: +# context_parts.append("## Related ICD Suggestions\n" + icd_fuzzy_context) +# if doc_context: +# context_parts.append("## Document Context\n" + doc_context) + +# context = "\n\n".join(context_parts) + +# # Initialize follow-up detection +# is_follow_up = False + +# # Check if this is a follow-up question +# if conv_history: +# last_interaction = conv_history[-1] +# last_question = last_interaction["question"].lower() +# last_answer = last_interaction.get("answer", "").lower() +# current_question = question.lower() + +# # Define meaningful keywords that indicate entity-related follow-ups +# entity_related_keywords = { +# "achievements", +# "awards", +# "accomplishments", +# "work", +# "contributions", +# "career", +# "company", +# "products", +# "life", +# "background", +# "education", +# "role", +# "experience", +# "history", +# "details", +# "places", +# "place", +# "information", +# "facts", +# "about", +# "birth", +# "death", +# "family", +# "books", +# "projects", +# "population", +# } + +# # Check if question is asking about attributes/achievements of previously discussed entity +# has_entity_attribute = any( +# word in current_question.split() for word in entity_related_keywords +# ) + +# # Extract entities from last answer to maintain context +# def extract_entities(text): +# # Split into words and get potential entities (capitalized words) +# words = text.split() +# entities = set() +# current_entity = [] + +# for word in words: +# if word[0].isupper(): +# current_entity.append(word) +# elif current_entity: +# if len(current_entity) > 0: +# entities.add(" ".join(current_entity)) +# current_entity = [] + +# if current_entity: +# entities.add(" ".join(current_entity)) +# return entities + +# last_entities = extract_entities(last_answer) + +# # Check for referential words +# referential_words = { +# "it", +# "this", +# "that", +# "these", +# "those", +# "they", +# "their", +# "he", +# "she", +# "him", +# "her", +# "his", +# "hers", +# "them", +# "there", +# "such", +# "its", +# } +# has_referential = any( +# word in referential_words for word in current_question.split() +# ) + +# # Calculate term overlap with both question and answer context +# def get_significant_terms(text): +# stop_words = { +# "what", +# "when", +# "where", +# "who", +# "why", +# "how", +# "is", +# "are", +# "was", +# "were", +# "be", +# "been", +# "the", +# "a", +# "an", +# "in", +# "on", +# "at", +# "to", +# "for", +# "of", +# "with", +# "by", +# "about", +# "as", +# "tell", +# "me", +# "please", +# } +# return set( +# word +# for word in text.split() +# if len(word) > 2 and word.lower() not in stop_words +# ) + +# current_terms = get_significant_terms(current_question) +# last_terms = get_significant_terms(last_question) +# answer_terms = get_significant_terms(last_answer) + +# # Include terms from both question and answer in context +# all_prev_terms = last_terms | answer_terms +# term_overlap = len(current_terms & all_prev_terms) +# total_terms = len(current_terms | all_prev_terms) +# term_similarity = term_overlap / total_terms if total_terms > 0 else 0 + +# # Enhanced follow-up detection combining multiple signals +# is_follow_up = ( +# has_referential +# or term_similarity +# >= 0.2 # Lower threshold when including answer context +# or ( +# has_entity_attribute and bool(last_entities) +# ) # Check if asking about attributes of known entity +# or ( +# last_interaction.get("type") == "general" +# and term_similarity >= 0.15 +# ) +# ) + +# logging.info(f"Follow-up analysis enhanced:") +# logging.info(f"- Referential words: {has_referential}") +# logging.info(f"- Term similarity: {term_similarity:.2f}") +# logging.info(f"- Entity attribute question: {has_entity_attribute}") +# logging.info(f"- Last entities found: {last_entities}") +# logging.info(f"- Is follow-up: {is_follow_up}") + +# # For entirely new topics (not follow-ups), use is_general_knowledge_question +# if not is_follow_up: +# if not context or is_general_knowledge_question(question, context, conv_history): +# logging.info("No relevant context or general knowledge question detected") +# answer = "

          No relevant information found in the hospital documents for this query.

          " +# if conversation_manager: +# await conversation_manager.add_rag_interaction( +# user_id, hospital_id, question, answer, session_id +# ) +# return {"answer": answer}, 200 + +# # Generate RAG answer with enhanced context +# prompt_template = f"""Based on the following context and conversation history, provide a detailed answer to the question. +# Previous conversation: +# {format_conversation_context(conv_history)} + +# Context from documents: +# {context} + +# Current question: {question} + +# Instructions: +# 1. When providing medical codes (ICD, CPT, etc.): +# - Always use the ICD codes listed in the sections titled "ICD Code Match" and "Related ICD Suggestions" from the context. +# - Do not use or invent ICD codes from your own training knowledge unless they appear in the provided context. +# - If multiple codes are relevant, return the one that best matches the user’s question. If unsure, return multiple options in HTML list format. +# - Remove all decimal points (e.g., use 'A150' instead of 'A15.0') +# - Format the response as: '

          The medical code for [condition] is [code]

          ' +# 2. Address the current question while maintaining conversation continuity +# 3. Resolve any ambiguous references using conversation history +# 4. Format the response in clear HTML + +# {html_instruction} +# {table_instruction if is_table_request(question) else ""} +# """ + +# response = await asyncio.to_thread( +# lambda: client.chat.completions.create( +# model="gpt-3.5-turbo-16k", +# messages=[ +# {"role": "system", "content": prompt_template}, +# {"role": "user", "content": question}, +# ], +# temperature=0.3, +# max_tokens=1000, +# ) +# ) + +# answer = ensure_html_response(response.choices[0].message.content) +# logging.info(f"Generated RAG answer for question: {question}") + +# # Store interaction in history +# if conversation_manager: +# await conversation_manager.add_rag_interaction( +# user_id, hospital_id, question, answer, session_id +# ) + +# return {"answer": answer}, 200 + +# except Exception as e: +# logging.error(f"Error in generate_answer_with_rag: {e}") +# return {"answer": f"

          Error: {str(e)}

          "}, 500 + +async def generate_answer_with_rag( + question, + hospital_id, + client, + doc_id=None, + user_id="default", + conversation_manager=None, + session_id=None, +): + """Generate an answer using RAG, strictly using provided document context and ICD data.""" + try: + html_instruction = """ + IMPORTANT: Format your ENTIRE response as HTML. Use appropriate HTML tags for all content: + - Use

          tags for paragraphs + - Use

          ,

          tags for headings and subheadings + - Use
            ,
          • tags for bullet points + - Use
              ,
            1. tags for numbered lists + - Use
              for quoted text + - Use for bold text and for emphasis + - If no relevant information is found in the provided context, respond ONLY with: +

              No relevant information found in the hospital documents for this query.

              + """ + + table_instruction = """ + - For tables, use proper HTML table structure: + + + + + + + {table_headers} + + + + {table_rows} + +
              {table_title}
              + """ + + # Get conversation history + conv_history = ( + conversation_manager.get_context_window(user_id, hospital_id, session_id) + if conversation_manager + else [] + ) + + # Generate contextual query + contextual_query = await generate_contextual_query( + question, user_id, hospital_id, conversation_manager + ) + + # Retrieve document context with strict relevance + doc_context = await get_relevant_context(contextual_query, hospital_id, doc_id) + + # Handle ICD context + icd_context = {} + if conv_history: + last_answer = conv_history[-1].get("answer", "") + icd_codes = re.findall(r"\b([A-Z][0-9A-Z]{2,6}[A-Z]?)\b", last_answer) + if icd_codes: + icd_context["last_code"] = icd_codes[0] + + is_icd_followup = False + if icd_context.get("last_code"): + followup_indicators = [ + "what causes", "what is causing", "why", "how", "symptoms", + "treatment", "diagnosis", "causes", "effects", "complications", + "risk factors", "prevention", "prognosis", "this", "disease", + "that", "it", + ] + is_icd_followup = any(indicator in question.lower() for indicator in followup_indicators) + if is_icd_followup: + icd_exact_context = get_icd_context_from_question(icd_context["last_code"], hospital_id) + icd_fuzzy_context = get_fuzzy_icd_context(f"{icd_context['last_code']} {question}", hospital_id) + else: + icd_exact_context = get_icd_context_from_question(question, hospital_id) + icd_fuzzy_context = get_fuzzy_icd_context(question, hospital_id) + else: + icd_exact_context = get_icd_context_from_question(question, hospital_id) + icd_fuzzy_context = get_fuzzy_icd_context(question, hospital_id) + + # Combine context with priority for ICD information + context_parts = [] + if is_icd_followup: + context_parts.append(f"## Previous ICD Code Context\nContinuing discussion about: {icd_context['last_code']}") + if icd_exact_context: + context_parts.append("## ICD Code Match\n" + icd_exact_context) + if icd_fuzzy_context: + context_parts.append("## Related ICD Suggestions\n" + icd_fuzzy_context) + if doc_context: + context_parts.append("## Document Context\n" + doc_context) + + context = "\n\n".join(context_parts) + logging.info(f"Total context length: {len(context.split())} words") + logging.info({context}) + + # Check context length + if len(doc_context.split()) == 0: + logging.info("A") + logging.info(f"Context too short ({len(context.split())} words), returning no information found") + answer = "

              No relevant information found in the hospital documents for this query.

              " + if conversation_manager: + await conversation_manager.add_rag_interaction( + user_id, hospital_id, question, answer, session_id + ) + return {"answer": answer}, 200 + + # Check if question lacks relevant context + if not context or is_general_knowledge_question(question, context, conv_history): + logging.info("B") + logging.info("No relevant context or general knowledge question detected") + answer = "

              No relevant information found in the hospital documents for this query.

              " + if conversation_manager: + await conversation_manager.add_rag_interaction( + user_id, hospital_id, question, answer, session_id + ) + return {"answer": answer}, 200 + + # Check follow-up status with stricter criteria + is_follow_up = False + if conv_history: + last_interaction = conv_history[-1] + last_question = last_interaction["question"].lower() + last_answer = last_interaction.get("answer", "").lower() + current_question = question.lower() + + # Define entity-related keywords + entity_related_keywords = { + "achievements", "awards", "accomplishments", "work", "contributions", + "career", "company", "products", "life", "background", "education", + "role", "experience", "history", "details", "places", "place", + "information", "facts", "about", "birth", "death", "family", + "books", "projects", "population", + } + + has_entity_attribute = any(word in current_question.split() for word in entity_related_keywords) + + # Extract entities using spaCy for better precision + doc_last = nlp(f"{last_question} {last_answer}") + doc_current = nlp(current_question) + last_entities = {ent.text.lower() for ent in doc_last.ents} + current_entities = {ent.text.lower() for ent in doc_current.ents} + + # Check for referential words + referential_words = { + "it", "this", "that", "these", "those", "they", "their", + "he", "she", "him", "her", "his", "hers", "them", "there", + "such", "its", + } + has_referential = any(word in referential_words for word in current_question.split()) + + # Calculate term overlap with stricter criteria + def get_significant_terms(text): + stop_words = { + "what", "when", "where", "who", "why", "how", "is", "are", + "was", "were", "be", "been", "the", "a", "an", "in", "on", + "at", "to", "for", "of", "with", "by", "about", "as", + "tell", "me", "please", + } + return set(word for word in text.split() if len(word) > 2 and word.lower() not in stop_words) + + current_terms = get_significant_terms(current_question) + last_terms = get_significant_terms(last_question) + answer_terms = get_significant_terms(last_answer) + all_prev_terms = last_terms | answer_terms + term_overlap = len(current_terms & all_prev_terms) + total_terms = len(current_terms | all_prev_terms) + term_similarity = term_overlap / total_terms if total_terms > 0 else 0 + + # Use spaCy similarity for follow-up detection + similarity = doc_current.similarity(doc_last) + + is_follow_up = ( + has_referential + or term_similarity >= 0.4 # Stricter threshold + or (has_entity_attribute and bool(last_entities & current_entities)) + or (last_interaction.get("type") == "general" and term_similarity >= 0.3) + ) + + logging.info(f"Follow-up analysis:") + logging.info(f"- Referential words: {has_referential}") + logging.info(f"- Term similarity: {term_similarity:.2f}") + logging.info(f"- Entity overlap: {bool(last_entities & current_entities)}") + logging.info(f"- SpaCy similarity: {similarity:.2f}") + logging.info(f"- Is follow-up: {is_follow_up}") + + # Generate answer with strict document-based instruction + prompt_template = f"""You are a document-based question-answering system. You must ONLY use the provided context and conversation history to answer the question. Do NOT use any external knowledge, assumptions, or definitions beyond the given context, even if the query seems familiar. If the context does not contain sufficient information to directly answer the question, respond ONLY with: +

              No relevant information found in the hospital documents for this query.

              + + Previous conversation: + {format_conversation_context(conv_history)} + + Context from documents: + {context} + + Current question: {question} + + Instructions: + 1. When providing medical codes (ICD, CPT, etc.): + - ONLY use the ICD codes listed in the sections titled "ICD Code Match" and "Related ICD Suggestions" from the context. + - Do not use or invent ICD codes from your own knowledge. + - If multiple codes are relevant, return the one that best matches the user’s question. If unsure, return multiple options in HTML list format. + - Remove all decimal points (e.g., use 'A150' instead of 'A15.0'). + - Format the response as: '

              The medical code for [condition] is [code]

              '. + 2. Address the current question while maintaining conversation continuity. + 3. Resolve any ambiguous references using conversation history. + 4. Format the response in clear HTML. + 5. Strictly adhere and provide a detailed answer only from the {context}.No extra knowledge or assumptions. + 6. Every answer must be detailed and only from the provided context. + 7. Answer should be 400-500 words long. + + {html_instruction} + {table_instruction if is_table_request(question) else ""} + """ + + response = await asyncio.to_thread( + lambda: client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": prompt_template}, + {"role": "user", "content": question}, + ], + temperature=0.1, # Lower temperature for strict adherence + max_tokens=1000, + ) + ) + + answer = ensure_html_response(response.choices[0].message.content) + logging.info(f"Generated RAG answer for question: {question}") + + # Store interaction in history + if conversation_manager: + await conversation_manager.add_rag_interaction( + user_id, hospital_id, question, answer, session_id + ) + + return {"answer": answer}, 200 + + except Exception as e: + logging.error(f"Error in generate_answer_with_rag: {e}") + return {"answer": f"

              Error: {str(e)}

              "}, 500 + +async def load_existing_vector_stores(): + """Load existing Chroma vector stores for each hospital""" + pool = await get_db_pool() + async with pool.acquire() as conn: + async with conn.cursor() as cursor: + try: + await cursor.execute("SELECT DISTINCT id FROM hospitals") + hospital_ids = [row[0] for row in await cursor.fetchall()] + + for hospital_id in hospital_ids: + try: + await initialize_or_load_vector_store(hospital_id) + except Exception as e: + logging.error( + f"Failed to load vector store for hospital {hospital_id}: {e}" + ) + continue + + except Exception as e: + logging.error(f"Error loading vector stores: {e}") + + +async def get_failed_page(doc_id): + pool = await get_db_pool() + async with pool.acquire() as conn: + async with conn.cursor() as cursor: + try: + await cursor.execute( + "SELECT failed_page FROM documents WHERE id = %s", (doc_id,) + ) + result = await cursor.fetchone() + return result[0] if result and result[0] else None + except Exception as e: + logging.error(f"Database error checking failed_page: {e}") + return None + + +async def update_document_status(doc_id, status, failed_page=None): + """Update document status with enum validation""" + if isinstance(status, str): + status = DocumentStatus[status.upper()].value + + pool = await get_db_pool() + async with pool.acquire() as conn: + async with conn.cursor() as cursor: + try: + if failed_page: + await cursor.execute( + "UPDATE documents SET processed_status = %s, failed_page = %s WHERE id = %s", + (status, failed_page, doc_id), + ) + else: + await cursor.execute( + "UPDATE documents SET processed_status = %s, failed_page = NULL WHERE id = %s", + (status, doc_id), + ) + await conn.commit() + return True + except Exception as e: + logging.error(f"Database update error: {e}") + return False + + +thread_pool = ThreadPoolExecutor(max_workers=10) + + +def async_to_sync(coroutine): + """Helper function to run async code in sync context""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(coroutine) + finally: + loop.close() + + +@app.route("/flask-api", methods=["GET"]) +def health_check(): + """Health check endpoint""" + access_logger.info(f"Health check request received from {request.remote_addr}") + return jsonify({"status": "ok"}), 200 + + +@app.route("/flask-api/process-pdf", methods=["POST"]) +def process_pdf(): + access_logger.info(f"PDF processing request received from {request.remote_addr}") + file_path = None + try: + file = request.files.get("pdf") + hospital_id = request.form.get("hospital_id") + doc_id = request.form.get("doc_id") + + logging.info( + f"Received PDF processing request for hospital {hospital_id}, doc_id {doc_id}" + ) + + if not all([file, hospital_id, doc_id]): + return jsonify({"error": "Missing required parameters"}), 400 + + def process_in_background(): + nonlocal file_path + try: + async_to_sync(update_document_status(doc_id, "processing")) + + # Add progress logging + logging.info(f"Starting processing of document {doc_id}") + + filename = f"doc_{doc_id}_{file.filename}" + file_path = os.path.join(uploads_dir, filename) + + with open(file_path, "wb") as f: + file.save(f) + + logging.info("Extracting PDF contents...") + content = extract_pdf_contents(file_path, int(hospital_id)) + + logging.info("Inserting content into database...") + metadata = {"filename": filename} + result = async_to_sync( + insert_content_into_db(content, metadata, doc_id) + ) + + if "error" in result: + async_to_sync(update_document_status(doc_id, "failed", 1)) + return False + + logging.info("Creating embeddings and indexing...") + success = async_to_sync(add_document_to_index(doc_id, hospital_id)) + + if success: + logging.info("Document processing completed successfully") + async_to_sync(update_document_status(doc_id, "processed")) + return True + else: + logging.error("Document processing failed during indexing") + async_to_sync(update_document_status(doc_id, "failed")) + return False + + except Exception as e: + logging.error(f"Processing error: {e}") + async_to_sync(update_document_status(doc_id, "failed")) + return False + finally: + if file_path and os.path.exists(file_path): + try: + os.remove(file_path) + except Exception as e: + logging.error(f"Error removing temporary file: {e}") + + # Execute processing and wait for result + future = thread_pool.submit(process_in_background) + success = future.result() + + if success: + return jsonify({"message": "Document processed successfully"}), 200 + else: + return jsonify({"error": "Document processing failed"}), 500 + + except Exception as e: + logging.error(f"API error: {e}") + if file_path and os.path.exists(file_path): + try: + os.remove(file_path) + except Exception as file_e: + logging.error(f"Error removing temporary file: {file_e}") + return jsonify({"error": str(e)}), 500 + + +# Initialize the hybrid conversation manager +redis_client = get_redis_client() +conversation_manager = HybridConversationManager(redis_client) + + +@app.route("/flask-api/generate-answer", methods=["POST"]) +def rag_answer_api(): + """Sync API endpoint for RAG-based question answering with conversation history.""" + access_logger.info(f"Generate answer request received from {request.remote_addr}") + try: + data = request.json + question = data.get("question", "").strip().lower() + hospital_code = data.get("hospital_code") + doc_id = data.get("doc_id") + user_id = data.get("user_id", "default") + session_id = data.get("session_id", None) + + logging.info(f"Received question from user {user_id}: {question}") + logging.info(f"Received hospital code: {hospital_code}") + logging.info(f"Received session_id: {session_id}") + + # is_confirmation_response = data.get("is_confirmation_response", False) + original_query = data.get("original_query", "") + + def process_rag_answer(): + try: + hospital_id = async_to_sync(get_hospital_id(hospital_code)) + logging.info(f"Resolved hospital ID: {hospital_id}") + + if not hospital_id: + return { + "error": "Invalid or missing 'hospital_code' in request" + }, 400 + + if original_query: + response_message = """ +

              I can only answer questions based on information found in the hospital documents.

              +

              The question you asked doesn't seem to be covered in the available documents.

              +

              You can try rephrasing your question or asking about a different topic.

              + """ + return {"answer": response_message}, 200 + + else: + # Regular RAG answer + return async_to_sync( + generate_answer_with_rag( + question=question, + hospital_id=hospital_id, + client=client, + doc_id=doc_id, + user_id=user_id, + conversation_manager=conversation_manager, # Pass the hybrid manager + session_id=session_id, + ) + ) + except Exception as e: + logging.error(f"Thread processing error: {str(e)}") + return {"error": str(e)}, 500 + + if not question: + return jsonify({"error": "Missing 'question' in request"}), 400 + + future = thread_pool.submit(process_rag_answer) + result, status_code = future.result() + + return jsonify(result), status_code + + except Exception as e: + logging.error(f"API error: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@app.route("/flask-api/delete-document-vectors", methods=["DELETE"]) +def delete_document_vectors_endpoint(): + """Endpoint to delete document vectors from ChromaDB""" + try: + data = request.json + hospital_id = data.get("hospital_id") + doc_id = data.get("doc_id") + + if not all([hospital_id, doc_id]): + return jsonify({"error": "Missing required parameters"}), 400 + + logging.info( + f"Received request to delete vectors for document {doc_id} from hospital {hospital_id}" + ) + + def process_deletion(): + try: + success = async_to_sync(delete_document_vectors(hospital_id, doc_id)) + if success: + return {"message": "Document vectors deleted successfully"}, 200 + else: + return {"error": "Failed to delete document vectors"}, 500 + except Exception as e: + logging.error(f"Error in vector deletion process: {e}") + return {"error": str(e)}, 500 + + future = thread_pool.submit(process_deletion) + result, status_code = future.result() + + return jsonify(result), status_code + + except Exception as e: + logging.error(f"API error: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@app.route("/flask-api/get-chroma-content", methods=["GET"]) +def get_chroma_content_endpoint(): + """API endpoint to get ChromaDB content by hospital_id""" + try: + hospital_id = request.args.get("hospital_id") + limit = int(request.args.get("limit", 30000)) + + if not hospital_id: + return jsonify({"error": "Missing required parameter: hospital_id"}), 400 + + def process_fetch(): + try: + result, status_code = async_to_sync( + get_chroma_content_by_hospital( + hospital_id=int(hospital_id), limit=limit + ) + ) + return result, status_code + except Exception as e: + logging.error(f"Error in ChromaDB fetch process: {e}") + return {"error": str(e)}, 500 + + future = thread_pool.submit(process_fetch) + result, status_code = future.result() + + return jsonify(result), status_code + + except Exception as e: + logging.error(f"API error: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +async def get_chroma_content_by_hospital(hospital_id: int, limit: int = 100): + """Fetch content from ChromaDB for a specific hospital""" + try: + # Initialize vector store + vector_store = await initialize_or_load_vector_store(hospital_id) + if not vector_store: + return {"error": "Vector store not found"}, 404 + + # Get collection + collection = vector_store._collection + + # Query the collection with hospital_id filter + results = await asyncio.to_thread( + lambda: collection.get(where={"hospital_id": str(hospital_id)}, limit=limit) + ) + + if not results or not results["ids"]: + return {"data": [], "count": 0}, 200 + + # Format the response + formatted_results = [] + for i in range(len(results["ids"])): + formatted_results.append( + { + "id": results["ids"][i], + "content": results["documents"][i], + "metadata": results["metadatas"][i], + } + ) + + return {"data": formatted_results, "count": len(formatted_results)}, 200 + + except Exception as e: + logging.error(f"Error fetching ChromaDB content: {e}") + return {"error": str(e)}, 500 + + +@app.before_request +def before_request(): + request._start_time = time.time() + + +@app.after_request +def after_request(response): + if hasattr(request, "_start_time"): + duration = time.time() - request._start_time + access_logger.info( + f'"{request.method} {request.path}" {response.status_code} - Duration: {duration:.3f}s - ' + f"IP: {request.remote_addr}" + ) + return response + + +if __name__ == "__main__": + logger.info("Starting SpurrinAI application") + logger.info(f"Python version: {sys.version}") + logger.info(f"Environment: {os.getenv('FLASK_ENV', 'production')}") + + try: + model_manager = ModelManager() + logger.info("Model manager initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize model manager: {e}") + sys.exit(1) + + # Initialize directories + os.makedirs(DATA_DIR, exist_ok=True) + os.makedirs(CHROMA_DIR, exist_ok=True) + logger.info(f"Initialized directories: {DATA_DIR}, {CHROMA_DIR}") + + # Clear Redis cache + redis_client = get_redis_client() + cleared_keys = 0 + for key in redis_client.scan_iter("vector_store_data:*"): + redis_client.delete(key) + cleared_keys += 1 + logger.info(f"Cleared {cleared_keys} Redis cache keys") + + # Load vector stores + logger.info("Loading existing vector stores...") + async_to_sync(load_existing_vector_stores()) + logger.info("Vector stores loaded successfully") + + # Start application + logger.info("Starting Flask application on port 5000") + app.run(port=5000, debug=False) \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..070c4ed --- /dev/null +++ b/docs/API.md @@ -0,0 +1,104 @@ +# API Documentation + +## Authentication +All API endpoints require authentication using JWT tokens. + +### Headers +``` +Authorization: Bearer +``` + +## Endpoints + +### Authentication +- `POST /api/users/hospital-users/login` - Generates userId, roleId and roleName from given user cridentials +- `GET /api/users/refresh-token/{{user_id}}/{{role_id}}` - Generates refresh token for hospitals and their users with roles namely Admin Superadmin, Spurrinadmin and Viewer +- `POST /api/users/get-access-token` - Generates access token for hospitals and their users with roles namely Admin, Superadmin and Viewer +- `POST /api/auth/refresh` - Generates access token for Spurrinadmin +- `POST /api/auth/login` - Login with token validation and hospital status check (for hospital users) + +### Spurrinadmin +- `GET /api/super-admin` - Get all super admins +- `POST /api/super-admin/initialize` - Add new super admin +- `DELETE /api/super-admin/:id` - Delete super admin + +### Hospitals +- `POST /api/hospitals/create-hospital` Create hospital +- `PUT /api/hospitals/update/:id` - Update hospital details +- `DELETE /api/hospitals/delete/:id` - Delete hospital +- `GET /api/hospitals/list` - Get list of hospitals +- `GET /api/hispitals/list/:{hospital_id}` - get hospital by id +- `GET /api/hospitals/users` - get list of hospital users +- `GET /api/hospitals/colors` - get colors from hospital + + SuperAdmin +- `POST /api/hospitals/send-temp-password` - send temporary password to email +- `POST /api/hospitals/change-password` - change the temporary password + + Admin and viewer +- `POST /api/hospitals/send-temp-password-av` - send temporary password to email +- `POST /api/hospitals/change-password-av` - send temporary password + +- `POST /api/hospitals/update-admin-name` - update admin name + +- `POST /api/hospitals/check-user-notification` - Check new app user notification regarding notification +- `PUT /api/hospitals/update-user-notification/:id` - Update app user notification status to checked (boolean) +- `POST /api/hospitals/interaction-logs` - Get interaction logs of hospital's app users + +- `PUT /api/hospitals/public-signup/:id` - Update allow public signup + +### Users + +- `POST /api/users/add-user` - add new user to hospital +- `PUT /api/users/edit-user/:id` - edit hospital user +- `delete /api/users/add-user` - delete hospital user +- `POST /api/upload-profile-photo` - upload profile photo +- `PUT /api/users/update-password/:id` - update password of user + +- `POST /api/users/get-spu-access-token` - Get SpurrinAdmin access token +- `POST /api/users/hospital-users/login` - Get hospital user ID +- `POST /api/users/logout` - User logout +- `GET /api/users/refresh-token/:user_id/:role_id` - Get refresh token by user ID + +### App Users +- `POST /api/app-users/signup` - App user registration +- `POST /api/app-users/login` - App user login +- `PUT /api/app-users/hitlike` - Like interaction +- `PUT /api/app-users/query-title` - Update query title +- `DELETE /api/app-users/query-title` - Delete query title +- `PUT /api/app-users/like-session` - Like session +- `PUT /api/app-users/approve-user/:appUserId` - Approve app user +- `DELETE /api/app-users/:userId` - Delete app user + +### Documents +- `PUT /api/documents/update-status/:id` - Update document status +- `DELETE /api/documents/delete/:id` - Delete document + +### Feedback +- `POST /api/feedbacks/app-user/submit` - Submit app user feedback + +### Analytics +- `POST /api/analytics/hospitals/active` - Get active hospitals analysis + +### Excel Data +- `POST /api/excel-data` - Upload bulk users + +### System +- `GET /health` - Health check endpoint +- `POST /api/sync-database` - Database synchronization (development only) +- `GET /` - Root endpoint + + +## Role-Based Access Control +Some endpoints require specific roles: +- Spurrinadmin - Role ID 6 +- Superadmin - Role ID 7 +- Admin - Role ID 8 +- Viewer - Role ID 9 + + +## File Upload +- Supported file types: Images, documents like pdf +- Upload directory: `/uploads/id_photos/` + `/uploads/documents/` + `/uploads/profile_photos` \ No newline at end of file diff --git a/model_manager.py b/model_manager.py new file mode 100644 index 0000000..0f7e2bc --- /dev/null +++ b/model_manager.py @@ -0,0 +1,139 @@ +from transformers import AutoTokenizer, AutoModel +from sentence_transformers import SentenceTransformer +import torch +from sklearn.metrics.pairwise import cosine_similarity +import logging +import atexit + +class ModelManager: + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super(ModelManager, cls).__new__(cls) + return cls._instance + + def __init__(self): + if not ModelManager._initialized: + logging.info("Initializing ModelManager - Loading models...") + self.load_models() + ModelManager._initialized = True + atexit.register(self.cleanup) + + def load_models(self): + try: + # Load models with specific device placement + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + logging.info(f"Using device: {self.device}") + + # Enable model caching + torch.hub.set_dir('./model_cache') + + # Load models with batch preparation + self.sentence_model = SentenceTransformer('all-MiniLM-L6-v2') + self.sentence_model.to(self.device) + self.sentence_model.eval() # Set to evaluation mode + + self.bert_tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased') + self.bert_model = AutoModel.from_pretrained('bert-base-uncased') + self.bert_model.to(self.device) + self.bert_model.eval() # Set to evaluation mode + + # Initialize embedding cache with batch support + self.embedding_cache = {} + self.max_cache_size = 10000 + self.batch_size = 32 # Optimize batch size + + logging.info("Models loaded successfully with batch optimization") + + except Exception as e: + logging.error(f"Error loading models: {e}") + raise + + def get_bert_embeddings(self, texts): + if isinstance(texts, str): + texts = [texts] + + # Process in batches + all_embeddings = [] + for i in range(0, len(texts), self.batch_size): + batch_texts = texts[i:i + self.batch_size] + + # Check cache for each text in batch + batch_embeddings = [] + uncached_texts = [] + uncached_indices = [] + + for idx, text in enumerate(batch_texts): + cache_key = f"bert_{hash(text)}" + if cache_key in self.embedding_cache: + batch_embeddings.append(self.embedding_cache[cache_key]) + else: + uncached_texts.append(text) + uncached_indices.append(idx) + + if uncached_texts: + inputs = self.bert_tokenizer(uncached_texts, return_tensors="pt", padding=True, truncation=True).to(self.device) + with torch.no_grad(): + outputs = self.bert_model(**inputs) + new_embeddings = outputs.last_hidden_state.mean(dim=1) + + # Cache new embeddings + for idx, text in enumerate(uncached_texts): + cache_key = f"bert_{hash(text)}" + if len(self.embedding_cache) < self.max_cache_size: + self.embedding_cache[cache_key] = new_embeddings[idx] + batch_embeddings.insert(uncached_indices[idx], new_embeddings[idx]) + + all_embeddings.extend(batch_embeddings) + + return torch.stack(all_embeddings) if len(all_embeddings) > 1 else all_embeddings[0].unsqueeze(0) + + def get_semantic_similarity(self, text1, text2): + # Check cache + cache_key = f"sim_{hash(text1)}_{hash(text2)}" + if cache_key in self.embedding_cache: + return self.embedding_cache[cache_key] + + # Preprocess texts for better matching + text1 = text1.lower().strip() + text2 = text2.lower().strip() + + # Enhanced batch process embeddings with context awareness + with torch.no_grad(): + # Sentence transformer similarity with increased weight + emb1 = self.sentence_model.encode([text1], batch_size=1, convert_to_numpy=True) + emb2 = self.sentence_model.encode([text2], batch_size=1, convert_to_numpy=True) + sent_sim = cosine_similarity(emb1, emb2)[0][0] + + # BERT similarity for deeper semantic understanding + bert_emb1 = self.get_bert_embeddings(text1).cpu().numpy() + bert_emb2 = self.get_bert_embeddings(text2).cpu().numpy() + bert_sim = cosine_similarity(bert_emb1, bert_emb2)[0][0] + + # Adjusted weights for better follow-up detection + similarity = 0.8 * sent_sim + 0.2 * bert_sim + + # Boost similarity for related context + if any(word in text2.split() for word in text1.split()): + similarity = min(1.0, similarity * 1.2) + + # Cache the result + if len(self.embedding_cache) < self.max_cache_size: + self.embedding_cache[cache_key] = similarity + + return similarity + + def cleanup(self): + """Cleanup models and free memory""" + logging.info("Cleaning up models...") + try: + del self.sentence_model + del self.bert_model + del self.bert_tokenizer + torch.cuda.empty_cache() if torch.cuda.is_available() else None + self.embedding_cache.clear() + logging.info("Models cleaned up successfully") + except Exception as e: + logging.error(f"Error during cleanup: {e}") diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..e64d28f --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": "js,json", + "ignore": ["node_modules", "public", "uploads"], + "exec": "node src/app.js" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3d2ca26 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8309 @@ +{ + "name": "spurrinai-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "spurrinai-backend", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "axios": "^1.7.9", + "bcrypt": "^5.1.1", + "compression": "^1.7.4", + "compromise": "^14.14.4", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "express-rate-limit": "^6.7.0", + "fast-levenshtein": "^3.0.0", + "form-data": "^4.0.1", + "helmet": "^7.0.0", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.0", + "multer": "^1.4.5-lts.1", + "mysql": "^2.18.1", + "mysql2": "^3.2.0", + "natural": "^8.0.1", + "node-cron": "^3.0.3", + "nodemailer": "^6.10.0", + "number-to-words": "^1.2.4", + "path": "^0.12.7", + "socket.io": "^4.8.1", + "stopword": "^3.1.4", + "string-similarity": "^4.0.4", + "uuid": "^11.0.5", + "winston": "^3.17.0", + "ws": "^8.13.0" + }, + "devDependencies": { + "eslint": "^8.38.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "nodemon": "^2.0.22", + "prettier": "^2.8.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", + "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", + "integrity": "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/afinn-165": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/afinn-165/-/afinn-165-1.0.4.tgz", + "integrity": "sha512-7+Wlx3BImrK0HiG6y3lU4xX7SpBPSSu8T9iguPMlaueRFxjbYwAQrp9lqZUuFikqKbd/en8lVREILvP2J80uJA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/afinn-165-financialmarketnews": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/afinn-165-financialmarketnews/-/afinn-165-financialmarketnews-3.0.0.tgz", + "integrity": "sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apparatus": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/apparatus/-/apparatus-0.0.10.tgz", + "integrity": "sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==", + "license": "MIT", + "dependencies": { + "sylvester": ">= 0.0.8" + }, + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compromise": { + "version": "14.14.4", + "resolved": "https://registry.npmjs.org/compromise/-/compromise-14.14.4.tgz", + "integrity": "sha512-QdbJwronwxeqb7a5KFK/+Y5YieZ4PE1f7ai0vU58Pp4jih+soDCBMuKVbhDEPQ+6+vI3vSiG4UAAjTAXLJw1Qw==", + "license": "MIT", + "dependencies": { + "efrt": "2.7.0", + "grad-school": "0.0.5", + "suffix-thumb": "5.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/efrt": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/efrt/-/efrt-2.7.0.tgz", + "integrity": "sha512-/RInbCy1d4P6Zdfa+TMVsf/ufZVotat5hCw3QXmWtjU+3pFEOvOQ7ibo3aIxyCJw2leIeAMjmPj+1SLJiCpdrQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.166", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", + "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz", + "integrity": "sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==", + "license": "MIT", + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/grad-school": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/grad-school/-/grad-school-0.0.5.tgz", + "integrity": "sha512-rXunEHF9M9EkMydTBux7+IryYXEZinRk6g8OBOGDBzo/qWJjhTxy86i5q7lQYpCLHN8Sqv1XX3OIOc7ka2gtvQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memjs": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/memjs/-/memjs-1.3.2.tgz", + "integrity": "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mongodb": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", + "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.3", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.1.tgz", + "integrity": "sha512-RhQ4DzmBi5BNGcS0w4u1vdMRIKcteXTCNzDt1j7XRcdWYBz1MjMjulBhPaeC5jBCHOD1yinuOFTTSOWLLGexWw==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.3", + "kareem": "2.6.3", + "mongodb": "~6.16.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "license": "MIT", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", + "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mysql2/node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/natural": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/natural/-/natural-8.1.0.tgz", + "integrity": "sha512-qHKU+BzPXzEDwToFBzlI+3oI2jeN3xRNP421ifoF2Fw7ej+5zEO3Z5wUKPjz00jhz9/ESerIUGfhPqqkOqlWPA==", + "license": "MIT", + "dependencies": { + "afinn-165": "^1.0.2", + "afinn-165-financialmarketnews": "^3.0.0", + "apparatus": "^0.0.10", + "dotenv": "^16.4.5", + "http-server": "^14.1.1", + "memjs": "^1.3.2", + "mongoose": "^8.2.0", + "pg": "^8.11.3", + "redis": "^4.6.13", + "safe-stable-stringify": "^2.2.0", + "stopwords-iso": "^1.1.0", + "sylvester": "^0.0.12", + "underscore": "^1.9.1", + "uuid": "^9.0.1", + "wordnet-db": "^3.1.11" + }, + "engines": { + "node": ">=0.4.10" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/number-to-words": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/number-to-words/-/number-to-words-1.2.4.tgz", + "integrity": "sha512-/fYevVkXRcyBiZDg6yzZbm0RuaD6i0qRfn8yr+6D0KgBMOndFPxuW10qCHpzs50nN8qKuv78k8MuotZhcVX6Pw==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.0", + "pg-pool": "^3.10.0", + "pg-protocol": "^1.10.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.5" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", + "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", + "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", + "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", + "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/portfinder": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/portfinder/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stopword": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/stopword/-/stopword-3.1.4.tgz", + "integrity": "sha512-RiyU12FwHWX1i42gxhn5sywgpV5mptZTzbHKqKh5wVDWC2Uu+8vKbLv8xxeNa0p29vECbYGLXpDayo54n9+dsg==", + "license": "MIT" + }, + "node_modules/stopwords-iso": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stopwords-iso/-/stopwords-iso-1.1.0.tgz", + "integrity": "sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-similarity": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", + "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "ISC" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/suffix-thumb": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/suffix-thumb/-/suffix-thumb-5.0.2.tgz", + "integrity": "sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sylvester": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.12.tgz", + "integrity": "sha512-SzRP5LQ6Ts2G5NyAa/jg16s8e3R7rfdFjizy1zeoecYWw+nGL+YA1xZvW/+iJmidBGSdLkuvdwTYEyJEb+EiUw==", + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordnet-db": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/wordnet-db/-/wordnet-db-3.1.14.tgz", + "integrity": "sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag==", + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..385de53 --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "spurrinai-backend", + "version": "1.0.0", + "description": "SpurrinAI Backend Node.js Application", + "main": "src/app.js", + "scripts": { + "start": "node src/app.js", + "dev": "nodemon src/app.js", + "test": "npm run test:unit && npm run test:integration", + "test:unit": "jest tests/unit", + "test:integration": "jest tests/integration", + "setup": "node scripts/setup.js", + "lint": "eslint src/**/*.js", + "lint:fix": "eslint src/**/*.js --fix", + "format": "prettier --write \"src/**/*.js\"", + "migrate": "node src/migrations/runMigrations.js", + "migrate:up": "node src/migrations/runMigrations.js up", + "migrate:down": "node src/migrations/runMigrations.js down", + "migrate:create": "node src/migrations/createMigration.js" + }, + "keywords": [], + "author": "Tech4biz Solutions", + "license": "UNLICENSED", + "private": true, + "dependencies": { + "axios": "^1.7.9", + "bcrypt": "^5.1.1", + "compression": "^1.7.4", + "compromise": "^14.14.4", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "express-rate-limit": "^6.7.0", + "fast-levenshtein": "^3.0.0", + "form-data": "^4.0.1", + "helmet": "^7.0.0", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.0", + "multer": "^1.4.5-lts.1", + "mysql": "^2.18.1", + "mysql2": "^3.2.0", + "natural": "^8.0.1", + "node-cron": "^3.0.3", + "nodemailer": "^6.10.0", + "number-to-words": "^1.2.4", + "path": "^0.12.7", + "socket.io": "^4.8.1", + "stopword": "^3.1.4", + "string-similarity": "^4.0.4", + "uuid": "^11.0.5", + "winston": "^3.17.0", + "ws": "^8.13.0" + }, + "devDependencies": { + "eslint": "^8.38.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "nodemon": "^2.0.22", + "prettier": "^2.8.7" + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/public/images/email-banner.png b/public/images/email-banner.png new file mode 100644 index 0000000..59b26c9 Binary files /dev/null and b/public/images/email-banner.png differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..190d7ba --- /dev/null +++ b/readme.md @@ -0,0 +1,200 @@ +# SpurrinAI Backend + +A Node.js backend application for SpurrinAI platform. + +## Project Structure + +``` +project-root/ +├── src/ # Source code +│ ├── app.js # App entry point +│ ├── config/ # Configuration files +│ ├── controllers/ # Route controllers +│ ├── middleware/ # Custom middleware +│ ├── migrations/ # Database migrations +│ ├── routes/ # Route definitions +│ ├── services/ # Business logic +│ └── utils/ # Utility functions +├── docs/ # Documentation +├── logs/ # Application logs +├── scripts/ # Build and setup scripts +├── tests/ # Test files +└── uploads/ # User uploads +``` + +## Prerequisites + +- Node.js >= 14.0.0 +- MySQL >= 5.7 +- npm >= 6.0.0 + +## Installation + +1. Clone the repository: +```bash +git clone -b dev https://git.tech4biz.wiki/Tech4Biz-Services/spurrin-cleaned-node.git +cd spurrinai-backend +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Set up environment variables: +```bash +cp .env.example .env +# Edit .env with your configuration +``` + +4. Run the setup script: +```bash +npm run setup +``` + +## Development Mode + +1. Start the development server with hot-reload: +```bash +npm run dev +``` + +2. Run tests: +```bash +# Run all tests +npm test + +# Run unit tests only +npm run test:unit + +# Run integration tests only +npm run test:integration +``` + +3. Code Quality: +```bash +# Lint code +npm run lint + +# Fix linting issues +npm run lint:fix + +# Format code +npm run format +``` + +## Production Mode + +1. Build the application: +```bash +npm run build +``` + +2. Start the production server: +```bash +npm start +``` + +3. For production deployment, ensure: + - Set `NODE_ENV=production` in `.env` + - Configure proper database credentials + - Set up SSL/TLS certificates + - Configure proper logging + - Set up process manager (PM2 recommended) + +### Using PM2 (Recommended for Production) + +1. Install PM2 globally: +```bash +npm install -g pm2 +``` + +2. Start the application with PM2: +```bash +pm2 start src/app.js --name spurrinai-backend +``` + +3. Other useful PM2 commands: +```bash +# Monitor application +pm2 monit + +# View logs +pm2 logs spurrinai-backend + +# Restart application +pm2 restart spurrinai-backend + +# Stop application +pm2 stop spurrinai-backend + +# Flush logs +pm2 flush + +# Delete all logs +pm2 flush spurrinai-backend + +# Reload application with zero downtime +pm2 reload spurrinai-backend +``` + +## Database Migrations + +```bash +# Create new migration +npm run migrate:create + +# Run all migrations +npm run migrate + +# Run migrations up +npm run migrate:up + +# Run migrations down +npm run migrate:down +``` + +## API Documentation + +Detailed API documentation can be found in the [docs/API.md](docs/API.md) file. + +## Environment Variables + +Required environment variables in `.env`: + +```env +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=spurrinai +DB_USER=postgres +DB_PASSWORD=your_password + +# JWT Configuration +JWT_SECRET=your_jwt_secret +JWT_EXPIRES_IN=24h + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your_email@gmail.com +SMTP_PASS=your_app_password + +# File Upload Configuration +UPLOAD_DIR=uploads +MAX_FILE_SIZE=5242880 # 5MB +``` + +## Support + +For support, please contact: +- Email: contact@tech4biz.io +- Issue Tracker: GitHub Issues + +## License + +UNLICENSED - All rights reserved by Tech4biz Solutions \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8694d19 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,32 @@ +# Standard library packages are not included (e.g., os, sys, threading) + +# Environment and utility +python-dotenv +tqdm + +# Flask and CORS +Flask +flask-cors + +# NLP and embeddings +spacy +nltk +openai +langchain +langchain-community +rapidfuzz + +# Redis +redis + +# Async MySQL +aiomysql + +# Concurrency +asyncio + +# Optional: For logging in structured or advanced environments +# (not strictly needed unless using specific logging handlers or formats) + +# Ensure spacy language model is installed (run manually post-install) +# python -m spacy download en_core_web_sm diff --git a/scripts/setup.js b/scripts/setup.js new file mode 100644 index 0000000..d8b55c5 --- /dev/null +++ b/scripts/setup.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Create necessary directories +const directories = [ + 'logs', + 'uploads', + 'uploads/documents', + 'uploads/id_photos', + 'uploads/profile_photos', + 'uploads/just_test', + 'tests/unit', + 'tests/integration', + 'docs' +]; + +directories.forEach(dir => { + const dirPath = path.join(__dirname, '..', dir); + try { + fs.mkdirSync(dirPath, { recursive: true }); + console.log(`Created directory: ${dir}`); + } catch (err) { + console.error(`Error creating directory ${dir}:`, err); + } +}); + +// Install dependencies +console.log('Installing dependencies...'); +execSync('npm install', { stdio: 'inherit' }); + +console.log('Setup completed successfully!'); diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..058f07c --- /dev/null +++ b/src/app.js @@ -0,0 +1,184 @@ +const express = require('express'); +const cors = require('cors'); +const compression = require('compression'); +const path = require('path'); +const dotenv = require('dotenv'); +const helmet = require('helmet'); +const initializeDatabase = require('./config/initDatabase'); + +// Load environment variables +dotenv.config(); + +// Import configurations +const config = require('./config'); +const { securityHeaders, apiLimiter, validateRequest, corsOptions } = require('./middlewares/security'); +const { errorHandler } = require('./middlewares/errorHandler'); +const logger = require('./utils/logger'); +const monitoring = require('./utils/monitoring'); + +// Import routes +const authRoutes = require('./routes/auth'); +const hospitalRoutes = require('./routes/hospitals'); +const userRoutes = require('./routes/users'); +const superAdminRoutes = require('./routes/superAdmins'); +const documentRoutes = require('./routes/documents'); +const onboardingRoutes = require('./routes/onboarding'); +const appUserRoutes = require('./routes/appUsers'); +const excelDataRoutes = require('./routes/exceldata'); +const feedbackRoute = require('./routes/feedbacks'); +const analyticsRoute = require('./routes/analysis'); + +// Import services +const { refreshExpiredTokens } = require('./services/cronJobs'); +const { repopulateQueueOnStartup } = require('./controllers/documentsController'); +require('./services/webSocket'); +require('./services/secondaryWebsocket'); + +// Create Express app +const app = express(); + +// Apply security middleware +app.use(helmet()); +app.use(securityHeaders); +app.use(compression({ + level: 6, + threshold: 1024, + filter: (req, res) => { + const contentType = res.getHeader('Content-Type'); + return /text|json|javascript|css/.test(contentType); + } +})); + +// Apply rate limiting to all API routes +app.use('/api/', apiLimiter); + +// Apply CORS +app.use(cors(corsOptions)); + +// Request validation +app.use(validateRequest); + +// Body parsing middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Static files +app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads'))); +app.use('/public', express.static(path.join(__dirname, '..', 'public'))); + +// Request logging middleware +app.use((req, res, next) => { + const start = Date.now(); + res.on('finish', () => { + const duration = Date.now() - start; + monitoring.trackRequest(req.path, req.method, res.statusCode, duration); + logger.info(`${req.method} ${req.path} ${res.statusCode} - ${duration}ms`); + }); + next(); +}); + +// Initialize database before starting the server +async function startServer() { + try { + // Initialize database + await initializeDatabase(); + console.log('Database initialized successfully'); + + // API routes + app.use('/api/auth', authRoutes); + app.use('/api/hospitals', hospitalRoutes); + app.use('/api/users', userRoutes); + app.use('/api/superAdmins', superAdminRoutes); + app.use('/api/onboarding', onboardingRoutes); + app.use('/api/documents', documentRoutes); + app.use('/api/app_users', appUserRoutes); + app.use('/api/process_excel', excelDataRoutes); + app.use('/api/feedbacks', feedbackRoute); + app.use('/api/analytics', analyticsRoute); + + // Health check endpoint + app.get('/health', (req, res) => { + res.json(monitoring.getHealthStatus()); + }); + + // Database sync endpoint (protected by environment check) + app.post('/api/sync-database', async (req, res) => { + try { + // Only allow in development or with proper authentication + if (process.env.NODE_ENV === 'development' || req.headers['x-sync-token'] === process.env.DB_SYNC_TOKEN) { + await initializeDatabase(); + res.json({ message: 'Database synchronized successfully' }); + } else { + res.status(403).json({ error: 'Unauthorized' }); + } + } catch (error) { + logger.error('Database sync failed:', error); + res.status(500).json({ error: 'Database synchronization failed' }); + } + }); + + // Root endpoint + app.get('/', (req, res) => { + res.send("SpurrinAI Backend is running!"); + }); + + // Error handling middleware + app.use(errorHandler); + + // Start server + const PORT = config.server.port; + const server = app.listen(PORT, () => { + logger.info(`Server is running on http://localhost:${PORT}`); + + // Initialize background tasks + refreshExpiredTokens(); + // repopulateQueueOnStartup(); + }); + + // Graceful shutdown + const gracefulShutdown = async () => { + logger.info('Received shutdown signal'); + + // Close server + server.close(() => { + logger.info('HTTP server closed'); + }); + + // Close database connections + const db = require('./config/database'); + await db.closePool(); + + // Close WebSocket connections + const wss = require('./services/webSocket'); + wss.close(() => { + logger.info('WebSocket server closed'); + }); + + process.exit(0); + }; + + process.on('SIGTERM', gracefulShutdown); + process.on('SIGINT', gracefulShutdown); + + // Handle uncaught exceptions + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error); + gracefulShutdown(); + }); + + // Handle unhandled promise rejections + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); + gracefulShutdown(); + }); + + return server; + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} + +startServer(); + +module.exports = app; \ No newline at end of file diff --git a/src/config/database.js b/src/config/database.js new file mode 100644 index 0000000..d8ef766 --- /dev/null +++ b/src/config/database.js @@ -0,0 +1,88 @@ +require('dotenv').config(); +const mysql = require('mysql2/promise'); +const config = require('./index'); + +// Create a connection pool +const pool = mysql.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + enableKeepAlive: true, + keepAliveInitialDelay: 0, + namedPlaceholders: true, + connectTimeout: 10000, + idleTimeout: 60000, + maxIdle: 10 +}); + +// Test the connection +pool.getConnection() + .then(connection => { + console.log('Database connected successfully'); + connection.release(); + }) + .catch(err => { + console.error('Error connecting to the database:', err); + process.exit(1); + }); + +// Handle pool errors +pool.on('error', (err) => { + console.error('Unexpected error on idle connection', err); + process.exit(-1); +}); + +// Query with retry logic +const queryWithRetry = async (sql, params, maxRetries = 3) => { + let lastError; + for (let i = 0; i < maxRetries; i++) { + try { + const [results] = await pool.query(sql, params); + return results; + } catch (error) { + lastError = error; + console.error(`Database query error (attempt ${i + 1}/${maxRetries}):`, error); + if (error.code === 'PROTOCOL_CONNECTION_LOST' || + error.code === 'ECONNRESET' || + error.code === 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR') { + console.log(`Database connection lost. Retry attempt ${i + 1} of ${maxRetries}`); + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + continue; + } + throw error; + } + } + throw lastError; +}; + +// Health check function +const checkDatabaseConnection = async () => { + try { + await pool.query('SELECT 1'); + return true; + } catch (error) { + console.error('Database health check failed:', error); + return false; + } +}; + +// Graceful shutdown +const closePool = async () => { + try { + await pool.end(); + console.log('Database pool closed successfully'); + } catch (error) { + console.error('Error closing database pool:', error); + throw error; + } +}; + +module.exports = { + query: queryWithRetry, + checkConnection: checkDatabaseConnection, + closePool +}; \ No newline at end of file diff --git a/src/config/emailConfig.js b/src/config/emailConfig.js new file mode 100644 index 0000000..71986be --- /dev/null +++ b/src/config/emailConfig.js @@ -0,0 +1,17 @@ +const nodemailer = require("nodemailer"); + +const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, // Zoho SMTP Server + port: process.env.SENDER_PORT, // Use 465 for SSL or 587 for TLS + secure: process.env.SENDER_SECURITY, // Set to true for port 465, false for port 587 + auth: { + user: process.env.EMAIL_USER, // Your Zoho email address + pass: process.env.EMAIL_PASS, // Your Zoho App Password (not your account password) + } + // tls: { + // minVersion: "TLSv1.2", + // ciphers: "SSLv3", + // }, +}); + +module.exports = transporter; \ No newline at end of file diff --git a/src/config/env.js b/src/config/env.js new file mode 100644 index 0000000..f87862d --- /dev/null +++ b/src/config/env.js @@ -0,0 +1,54 @@ +require('dotenv').config(); + +const env = { + NODE_ENV: process.env.NODE_ENV || 'development', + PORT: process.env.PORT || 3000, + + // JWT Configuration + JWT_ACCESS_TOKEN_SECRET: process.env.JWT_ACCESS_TOKEN_SECRET, + JWT_REFRESH_TOKEN_SECRET: process.env.JWT_REFRESH_TOKEN_SECRET, + JWT_ACCESS_TOKEN_EXPIRY : process.env.JWT_ACCESS_TOKEN_EXPIRY, + JWT_REFRESH_TOKEN_EXPIRY: process.env.JWT_REFRESH_TOKEN_EXPIRY, + + // Email Configuration + EMAIL_USER: process.env.EMAIL_USER, + EMAIL_PASS: process.env.EMAIL_PASS, + EMAIL_HOST: process.env.EMAIL_HOST , + EMAIL_PORT: process.env.SENDER_PORT, + BACK_URL: process.env.BACK_URL, + + // Database Configuration + DB_HOST: process.env.DB_HOST, + DB_USER: process.env.DB_USER, + DB_PASSWORD: process.env.DB_PASSWORD, + DB_NAME: process.env.DB_NAME, + + // API Configuration + BACK_URL: process.env.BACK_URL, + DOMAIN_URL: process.env.DOMAIN_url, + FLASK_BASE_URL: process.env.FLASK_BASE_URL +}; + +// Group required environment variables by feature +const requiredEnvVars = { + email: ['EMAIL_USER', 'EMAIL_PASS', 'EMAIL_HOST','EMAIL_PORT', 'BACK_URL'], + database: ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'], + jwt: ['JWT_ACCESS_TOKEN_SECRET', 'JWT_REFRESH_TOKEN_SECRET', 'JWT_ACCESS_TOKEN_EXPIRY', 'JWT_REFRESH_TOKEN_EXPIRY'], + api: ['BACK_URL', 'DOMAIN_URL', 'FLASK_BASE_URL'] +}; + +// Validate required environment variables based on feature +const validateEnvVars = (feature) => { + const vars = requiredEnvVars[feature] || []; + const missingVars = vars.filter(envVar => !env[envVar]); + + if (missingVars.length > 0) { + throw new Error(`Missing required environment variables for ${feature}: ${missingVars.join(', ')}`); + } +}; + +// Export validation function along with env object +module.exports = { + env, + validateEnvVars +}; \ No newline at end of file diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..bc66243 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,79 @@ + + + +const config = { + + development: { + database: { + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'hospital_management', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + enableKeepAlive: true, + keepAliveInitialDelay: 0, + namedPlaceholders: true, + connectTimeout: 10000, + idleTimeout: 60000, + maxIdle: 10 + }, + server: { + port: process.env.PORT || 3000, + cors: { + origin: [ + 'http://localhost:5173', + 'http://localhost:5174', + 'http://localhost:3000', + 'http://localhost:8081', + 'http://testhospital.localhost:5174', + 'http://testhospitaltwo.localhost:5174', + ] + } + }, + websocket: { + port: 40510, + perMessageDeflate: false + } + }, + production: { + database: { + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + waitForConnections: true, + connectionLimit: 20, + queueLimit: 0, + enableKeepAlive: true, + keepAliveInitialDelay: 0, + namedPlaceholders: true, + connectTimeout: 10000, + idleTimeout: 60000, + maxIdle: 20 + }, + server: { + port: process.env.PORT || 3000, + cors: { + origin: [ + 'https://spurrinai.com', + 'https://www.spurrinai.com', + 'https://spurrinai.info', + 'https://www.spurrinai.info', + 'http://www.spurrinai.info', + 'https://spurrinai.org', + 'https://www.spurrinai.org' + ] + } + }, + websocket: { + port: 40510, + perMessageDeflate: false + } + + + } +}; + +module.exports = config[process.env.NODE_ENV || 'development']; \ No newline at end of file diff --git a/src/config/initDatabase.js b/src/config/initDatabase.js new file mode 100644 index 0000000..d011c24 --- /dev/null +++ b/src/config/initDatabase.js @@ -0,0 +1,308 @@ +const mysql = require('mysql2/promise'); +require('dotenv').config(); + +async function initializeDatabase() { + let connection; + try { + // First, connect without database to create it if it doesn't exist + connection = await mysql.createConnection({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + }); + + // Check if database exists + const rows = await connection.query('SHOW DATABASES LIKE ?', [process.env.DB_NAME || 'spurrinai']); + const dbExists = rows.length > 0; + + // Create database if it doesn't exist + if (!dbExists) { + await connection.query(`CREATE DATABASE ${process.env.DB_NAME || 'spurrinai'}`); + console.log(`Database ${process.env.DB_NAME || 'spurrinai'} created successfully`); + } + + // Switch to the database + await connection.query(`USE ${process.env.DB_NAME || 'spurrinai'}`); + + // Create tables in the correct order + const tables = [ + // Roles table first (no dependencies) + `CREATE TABLE IF NOT EXISTS roles ( + id INT NOT NULL AUTO_INCREMENT, + name ENUM('Spurrinadmin', 'Superadmin', 'Admin', 'Viewer') NOT NULL, + description_role TEXT, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY name (name) + )`, + + // Super admins table + `CREATE TABLE IF NOT EXISTS super_admins ( + id INT NOT NULL AUTO_INCREMENT, + email VARCHAR(255) NOT NULL, + hash_password VARCHAR(255) DEFAULT NULL, + role_id INT DEFAULT NULL, + expires_at DATETIME DEFAULT NULL, + type VARCHAR(50) DEFAULT NULL, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + refresh_token TEXT, + access_token VARCHAR(500) DEFAULT NULL, + access_token_expiry DATETIME DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY email (email), + KEY fk_super_admin_role_id (role_id), + CONSTRAINT fk_super_admin_role_id FOREIGN KEY (role_id) REFERENCES roles (id) + )`, + + // Hospitals table + `CREATE TABLE IF NOT EXISTS hospitals ( + id INT NOT NULL AUTO_INCREMENT, + name_hospital VARCHAR(255) NOT NULL, + subdomain VARCHAR(255) NOT NULL, + primary_admin_email VARCHAR(255) NOT NULL, + primary_admin_password VARCHAR(255) NOT NULL, + expires_at DATETIME DEFAULT NULL, + type VARCHAR(50) DEFAULT NULL, + primary_color VARCHAR(20) DEFAULT NULL, + secondary_color VARCHAR(20) DEFAULT NULL, + logo_url TEXT, + status ENUM('Active', 'Inactive') DEFAULT 'Active', + onboarding_status ENUM('Pending', 'Completed') DEFAULT 'Pending', + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + admin_name VARCHAR(255) NOT NULL, + mobile_number VARCHAR(15) NOT NULL, + location VARCHAR(255) NOT NULL, + super_admin_id INT NOT NULL, + hospital_code VARCHAR(12) NOT NULL, + publicSignupEnabled BOOLEAN DEFAULT FALSE, + PRIMARY KEY (id), + UNIQUE KEY subdomain (subdomain), + UNIQUE KEY hospital_code (hospital_code), + KEY fk_super_admin_id (super_admin_id), + CONSTRAINT fk_super_admin_id FOREIGN KEY (super_admin_id) REFERENCES super_admins (id) ON DELETE CASCADE ON UPDATE CASCADE + )`, + + // Hospital users table + `CREATE TABLE IF NOT EXISTS hospital_users ( + id INT NOT NULL AUTO_INCREMENT, + hospital_id INT DEFAULT NULL, + email VARCHAR(255) NOT NULL, + hash_password VARCHAR(255) NOT NULL, + expires_at DATETIME DEFAULT NULL, + type VARCHAR(50) DEFAULT NULL, + role_id INT DEFAULT NULL, + is_default_admin TINYINT(1) DEFAULT '1', + requires_onboarding TINYINT(1) DEFAULT '1', + password_reset_required TINYINT(1) DEFAULT '1', + profile_photo_url TEXT, + phone_number VARCHAR(15) DEFAULT NULL, + bio TEXT, + status ENUM('Active', 'Inactive') DEFAULT 'Active', + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + refresh_token TEXT, + name VARCHAR(255) DEFAULT NULL, + department VARCHAR(255) DEFAULT NULL, + location VARCHAR(255) DEFAULT NULL, + mobile_number VARCHAR(15) DEFAULT NULL, + access_token VARCHAR(500) DEFAULT NULL, + access_token_expiry DATETIME DEFAULT NULL, + hospital_code VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (id), + UNIQUE KEY email (email), + KEY hospital_id (hospital_id), + KEY role_id (role_id), + CONSTRAINT hospital_users_ibfk_1 FOREIGN KEY (hospital_id) REFERENCES hospitals (id), + CONSTRAINT hospital_users_ibfk_2 FOREIGN KEY (role_id) REFERENCES roles (id) + )`, + + // App users table + `CREATE TABLE IF NOT EXISTS app_users ( + id INT NOT NULL AUTO_INCREMENT, + email VARCHAR(255) NOT NULL, + hash_password VARCHAR(255) NOT NULL, + pin_number VARCHAR(4) DEFAULT NULL, + pin_enabled BOOLEAN DEFAULT FALSE, + remember_me BOOLEAN DEFAULT FALSE, + username TEXT, + upload_status ENUM('0', '1') DEFAULT '0', + status ENUM('Pending', 'Active', 'Inactive') DEFAULT 'Pending', + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + hospital_code VARCHAR(12) DEFAULT NULL, + id_photo_url TEXT, + query_title TEXT NULL DEFAULT NULL, + otp_code VARCHAR(6) DEFAULT NULL, + otp_expires_at DATETIME DEFAULT NULL, + access_token TEXT, + access_token_expiry DATETIME DEFAULT NULL, + checked BOOLEAN DEFAULT 0, + PRIMARY KEY (id), + UNIQUE KEY email (email), + KEY fk_hospital_code (hospital_code), + CONSTRAINT fk_hospital_code FOREIGN KEY (hospital_code) REFERENCES hospitals (hospital_code) + )`, + + // Documents table + `CREATE TABLE IF NOT EXISTS documents ( + id INT NOT NULL AUTO_INCREMENT, + hospital_id INT DEFAULT NULL, + uploaded_by INT DEFAULT NULL, + file_name VARCHAR(255) NOT NULL, + file_url TEXT NOT NULL, + uploaded_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + processed_status ENUM('Pending', 'Processed', 'Failed') DEFAULT 'Pending', + failed_page INT DEFAULT NULL, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + reason TEXT, + PRIMARY KEY (id), + KEY hospital_id (hospital_id), + KEY uploaded_by (uploaded_by), + CONSTRAINT documents_ibfk_1 FOREIGN KEY (hospital_id) REFERENCES hospitals (id), + CONSTRAINT documents_ibfk_2 FOREIGN KEY (uploaded_by) REFERENCES hospital_users (id) + )`, + + // Document metadata table + `CREATE TABLE IF NOT EXISTS document_metadata ( + id INT NOT NULL AUTO_INCREMENT, + document_id INT DEFAULT NULL, + key_name VARCHAR(100) DEFAULT NULL, + value_name TEXT, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY document_id (document_id), + CONSTRAINT document_metadata_ibfk_1 FOREIGN KEY (document_id) REFERENCES documents (id) + )`, + + // Document pages table + `CREATE TABLE IF NOT EXISTS document_pages ( + id INT NOT NULL AUTO_INCREMENT, + document_id INT NOT NULL, + page_number INT NOT NULL, + content LONGTEXT, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY document_id (document_id), + CONSTRAINT document_pages_ibfk_1 FOREIGN KEY (document_id) REFERENCES documents (id) ON DELETE CASCADE + )`, + + // Questions answers table + `CREATE TABLE IF NOT EXISTS questions_answers ( + id INT NOT NULL AUTO_INCREMENT, + document_id INT DEFAULT NULL, + question TEXT NOT NULL, + answer TEXT NOT NULL, + type ENUM('Text', 'Graph', 'Image', 'Chart') DEFAULT 'Text', + views INT DEFAULT 0, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY document_id (document_id), + CONSTRAINT questions_answers_ibfk_1 FOREIGN KEY (document_id) REFERENCES documents (id) + )`, + + // Interaction logs table + `CREATE TABLE IF NOT EXISTS interaction_logs ( + id INT NOT NULL AUTO_INCREMENT, + session_id INT DEFAULT NULL, + session_title TEXT NOT NULL, + app_user_id INT DEFAULT NULL, + status ENUM('Active', 'Inactive') NOT NULL DEFAULT 'Active', + query TEXT NOT NULL, + response TEXT NOT NULL, + is_liked BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + hospital_code VARCHAR(12) NOT NULL, + PRIMARY KEY (id), + KEY session_id (session_id) + )`, + + // QA runtime cache table + `CREATE TABLE IF NOT EXISTS qa_runtime_cache ( + id INT NOT NULL AUTO_INCREMENT, + hospital_id INT DEFAULT NULL, + query TEXT NOT NULL, + generated_answer TEXT, + cached_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY hospital_id (hospital_id), + CONSTRAINT qa_runtime_cache_ibfk_1 FOREIGN KEY (hospital_id) REFERENCES hospitals (id) + )`, + + // Onboarding steps table + `CREATE TABLE IF NOT EXISTS onboarding_steps ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT DEFAULT NULL, + step ENUM('Pending', 'PasswordChanged', 'AssetsUploaded', 'ColorUpdated', 'Completed') DEFAULT 'Pending', + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY user_id (user_id), + CONSTRAINT onboarding_steps_ibfk_1 FOREIGN KEY (user_id) REFERENCES hospital_users (id) + )`, + + // Feedback table + `CREATE TABLE IF NOT EXISTS feedback ( + feedback_id INT AUTO_INCREMENT PRIMARY KEY, + sender_type ENUM('appuser', 'hospital') NOT NULL, + sender_id INT NOT NULL, + receiver_type ENUM('hospital', 'spurrin') NOT NULL, + receiver_id INT NOT NULL, + rating ENUM('Terrible', 'Bad', 'Okay', 'Good', 'Awesome') NOT NULL, + purpose TEXT NOT NULL, + information_received ENUM('Yes', 'Partially', 'No') NOT NULL, + feedback_text TEXT, + improvement TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`, + + // Audit logs table + `CREATE TABLE IF NOT EXISTS audit_logs ( + id INT NOT NULL AUTO_INCREMENT, + user_id INT DEFAULT NULL, + table_name VARCHAR(255) DEFAULT NULL, + operation ENUM('INSERT', 'UPDATE', 'DELETE') DEFAULT NULL, + changes_log JSON DEFAULT NULL, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) + )` + ]; + + // Execute all table creation queries + for (const table of tables) { + await connection.query(table); + } + + // Insert default roles if they don't exist + const defaultRoles = [ + [6, 'Spurrinadmin', 'Spurrin admin access'], + [7, 'Superadmin', 'Administrator with access to manage all functionalities of a hospital including managing hospital assets.'], + [8, 'Admin', 'Administrator with access to manage all functionalities of a hospital.'], + [9, 'Viewer', 'User with read-only access.'] + ]; + + for (const [id, name, description] of defaultRoles) { + await connection.query( + 'INSERT IGNORE INTO roles (id, name, description_role) VALUES (?, ?, ?)', + [id, name, description] + ); + } + + console.log('Database initialization completed successfully'); + } catch (error) { + console.error('Error initializing database:', error); + throw error; + } finally { + if (connection) { + await connection.end(); + } + } +} + +module.exports = initializeDatabase; \ No newline at end of file diff --git a/src/controllers/.DS_Store b/src/controllers/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/controllers/.DS_Store differ diff --git a/src/controllers/analysisController.js b/src/controllers/analysisController.js new file mode 100644 index 0000000..a8048cf --- /dev/null +++ b/src/controllers/analysisController.js @@ -0,0 +1,99 @@ +const analysisService = require('../services/analysisService'); + +// Get analysis of onboarded hospitals +exports.getOnboardedHospitalsAnalysis = async (req, res) => { + try { + // Check authorization + if (req.user.role !== 'Spurrinadmin' && req.user.role !== 6) { + return res.status(403).json({ + error: "You are not authorized!" + }); + } + + const response = await analysisService.getOnboardedHospitalsAnalysis(); + + res.status(200).json({ + message: "All hospitals analysis fetched successfully", + data: response + }); + + } catch (error) { + console.error("Error fetching hospitals analysis:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +// Get active hospitals and their app users in a selected period +exports.getActiveHospitalsAnalysis = async (req, res) => { + try { + // Check authorization + if(req.user.role !== 'Spurrinadmin' && req.user.role !== 6){ + return res.status(403).json({ + error: "You are not authorized!" + }); + } + + const { start_date, end_date } = req.body; + const response = await analysisService.getActiveHospitalsAnalysis(start_date, end_date); + + res.status(200).json({ + message: "Active hospitals analysis fetched successfully", + data: response + }); + + } catch (error) { + console.error("Error fetching active hospitals analysis:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +// Get active chat users analysis +exports.getActiveChatUsersAnalysis = async (req, res) => { + try { + // Check authorization + if(req.user.role !== 'Spurrinadmin' && req.user.role !== 6){ + return res.status(403).json({ + error: "You are not authorized!" + }); + } + + const { start_date, end_date } = req.body; + const response = await analysisService.getActiveChatUsersAnalysis(start_date, end_date); + + res.status(200).json({ + message: "Active chat users analysis fetched successfully", + data: response + }); + + } catch (error) { + console.error("Error fetching active chat users analysis:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +// Get total registered users per hospital (accumulative) +exports.getHospitalRegisteredUsers = async (req, res) => { + try { + const { hospitalId } = req.params; + const { start_date, end_date } = req.query; + + const result = await analysisService.getHospitalRegisteredUsers(hospitalId, start_date, end_date); + res.json(result); + } catch (error) { + console.error('Error in getHospitalRegisteredUsers:', error); + res.status(500).json({ error: error.message }); + } +}; + +// Get active users per hospital in selected period +exports.getHospitalActiveUsers = async (req, res) => { + try { + const { start_date, end_date } = req.query; + + const result = await analysisService.getHospitalActiveUsers(start_date, end_date); + res.json(result); + } catch (error) { + console.error('Error in getHospitalActiveUsers:', error); + res.status(500).json({ error: error.message }); + } +}; \ No newline at end of file diff --git a/src/controllers/appUserController.js b/src/controllers/appUserController.js new file mode 100644 index 0000000..628cb6b --- /dev/null +++ b/src/controllers/appUserController.js @@ -0,0 +1,1020 @@ +const bcrypt = require("bcrypt"); +const fs = require("fs"); +const jwt = require("jsonwebtoken"); +const nlp = require("compromise"); +const path = require('path') +const transporter = require('../config/emailConfig'); +const generatePasswordResetEmail = require('../templates/passwordResetEmail'); +const appUserService = require('../services/appUserService'); + +exports.initWebSocket = (io) => { + io.on("connection", (socket) => { + + socket.on("register_hospital", (hospitalCode) => { + hospitalSockets[hospitalCode] = socket; + }); + + socket.on("disconnect", () => { + const hospitalCode = Object.keys(hospitalSockets).find( + (key) => hospitalSockets[key] === socket + ); + if (hospitalCode) delete hospitalSockets[hospitalCode]; + }); + }); +}; + +exports.uploadIdPhoto = async (req, res) => { + try { + const userId = parseInt(req.params.id, 10); + const { hospital_code } = req.user; + + if (isNaN(userId)) { + console.error("Invalid User ID:", userId); + return res.status(400).json({ error: "Invalid user ID" }); + } + + if (!req.file) { + console.error("No file uploaded"); + return res.status(400).json({ error: "No file uploaded" }); + } + + const photoPath = `/uploads/id_photos/${req.file.filename}`; + const result = await appUserService.uploadIdPhoto(userId, hospital_code, photoPath); + + res.status(200).json({ + message: "ID photo uploaded successfully!", + id_photo_url: result, + }); + } catch (error) { + console.error("Error uploading ID photo:", error.message); + if (error.message.includes("not authorized")) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.updateSettings = async (req, res) => { + try { + const userId = req.user.id; + const { pin, pin_enabled, remember_me } = req.body; + + const result = await appUserService.updateSettings(userId, { pin, pin_enabled, remember_me }); + + res.status(200).json({ + message: 'Settings updated successfully.', + accessToken: result.accessToken + }); + } catch (error) { + console.error('Error updating settings:', error); + if (error.message.includes('Invalid PIN format')) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; + +exports.hitlike = async (req, res) => { + try { + const { session_id, id } = req.body; + const log_id = id; + const app_user_id = req.user.id; + + if (!app_user_id || !session_id || !log_id) { + return res.status(400).json({ + status: 'error', + message: 'app user id and session id and log id are required', + }); + } + + const result = await appUserService.toggleLike(app_user_id, session_id, log_id); + + return res.status(200).json({ + status: 'success', + message: 'Like updated successfully', + data: result + }); + } catch (error) { + console.error('Error during like toggle:', error); + if (error.message.includes('No matching record')) { + return res.status(404).json({ + status: 'error', + message: error.message + }); + } + return res.status(500).json({ + status: 'error', + message: 'Internal server error', + }); + } +}; + +exports.signup = async (req, res) => { + try { + const { pin, pin_status, remember_me, email, password, hospital_code, username } = req.body; + + if (!req.file) { + return res.status(400).json({ error: "ID photo url is required" }); + } + + const userId = await appUserService.signup({ + email, + password, + hospital_code, + username, + pin, + pin_status, + remember_me + }); + + res.status(201).json({ + message: "User registered successfully", + user_id: userId + }); + } catch (error) { + console.error("Error during signup:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("Invalid hospital code")) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes("already in use")) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.login = async (req, res) => { + try { + const { email, password } = req.body; + let remember_me; + + if (!email || !password) { + return res.status(400).json({ error: "Email and password are required" }); + } + + // Check if the user exists + const result = await appUserService.login_getUserByEmail(email); + + if (result.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + const user = result[0]; + remember_me = user.remember_me + + const hospitalData = await appUserService.login_getHospitalByCode(user.hospital_code); + // if (hospitalData.publicSignupEnabled) { + // throw new Error("Hospital not found"); + // } + + + let actual_status; + actual_status = user.status + if(hospitalData[0].publicSignupEnabled){ + user.status = "Active" + } + + // if (user.status === "Pending" && !hospitalData[0].publicSignupEnabled) { + // return res.status(404).json({ message: "Your Account is Under Review" }); + + // } + + // if (user.status === "Inactive" && !hospitalData[0].publicSignupEnabled) { + // return res.status(404).json({ message: "Contact admin your account is not approved" }); + + // } + + + // Validate the password + const validPassword = await bcrypt.compare(password, user.hash_password); + if (!validPassword) { + return res.status(401).json({ error: "Invalid email or password" }); + } + + // Determine expiry based on remember_me + let expiresIn = "5h"; + let expiryTimestamp = new Date(); + let rememberMeValue = remember_me; + if (rememberMeValue === undefined) rememberMeValue = user.remember_me; + if (rememberMeValue === true || rememberMeValue === 1 || rememberMeValue === "1" || rememberMeValue === "true") { + await appUserService.login_updateRememberMe(remember_me, user.id); + expiresIn = "30d"; + expiryTimestamp.setDate(expiryTimestamp.getDate() + 30); + } else { + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + } + + // Generate a new access token + const payload = { id: user.id, email: user.email, role: "AppUser" }; + const accessToken = jwt.sign(payload, process.env.JWT_ACCESS_TOKEN_SECRET, { + expiresIn: expiresIn, + }); + + // SQL query to fetch hospital data based on the provided hospital_code + const result_hospital = await appUserService.login_getHospitalByCode(user.hospital_code); + + // hospital users + const resultHospitalUsr = await appUserService.login_getHospitalUsersByCode(user.hospital_code); + + const hospitalUser = resultHospitalUsr[0]; + if (!hospitalUser) { + return res.status(404).json({ error: "Hospital user not found" }); + } + // Update the access token and expiry in the database + await appUserService.login_updateAccessToken(accessToken, expiryTimestamp, (rememberMeValue === true || rememberMeValue === 1 || rememberMeValue === "1" || rememberMeValue === "true") ? 1 : 0, user.id); + + + // Send response + res.status(200).json({ + message: "Login successful", + user: { + id: user.id, + email: user.email, + pin: user.pin_number, + pin_enabled: user.pin_enabled, + hospital_code: user.hospital_code, + status: user.status, + actualStatus: actual_status, + hospital_name: result_hospital[0].name_hospital, + primary_color: result_hospital[0].primary_color, + secondary_color: result_hospital[0].secondary_color, + upload_status: user.upload_status, + id_photo_url: user.id_photo_url, + primary_admin_email: result_hospital[0].primary_admin_email, + mobile_number: result_hospital[0].mobile_number, + username: user.username, + logo_url: resultHospitalUsr[0].profile_photo_url, + query_title: user.query_title, + remember_me: (rememberMeValue === true || rememberMeValue === 1 || rememberMeValue === "1" || rememberMeValue === "true") ? 1 : 0 + }, + accessToken, + }); + } catch (error) { + console.error("Error during login:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.checkPin = async (req, res) => { + try { + const { email, pin } = req.body; + + if (!email || !pin) { + return res.status(400).json({ error: "Email and Pin are required" }); + } + + // Check if the user exists + const result = await appUserService.checkPin_getUserByEmail(email); + + if (result.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + const user = result[0]; + + const hospitalData = await appUserService.checkPin_getHospitalByCode(user.hospital_code); + + + + + if (user.status === "Pending" && !hospitalData[0].publicSignupEnabled) { + throw new Error("Your Account is Under Review"); + } + + if (user.status === "Inactive" && !hospitalData[0].publicSignupEnabled) { + throw new Error("Contact admin your account is not approved"); + } + // Check PIN if PIN status is enabled + if (pin !== user.pin_number) { + return res.status(401).json({ + error: "Invalid PIN", + requires_pin: true, + message: "Incorrect PIN. Please try again" + }); + } + + // Send response + res.status(200).json({ + message: "Pin verified successfully", + }); + } catch (error) { + console.error("Error during login:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.logout = async (req, res) => { + try { + const userId = req.user.id; // Assuming the user is authenticated via middleware + + // Check if the user exists + const result = await appUserService.logout_getUserById(userId); + + if (result.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + // Invalidate the access token in the database + await appUserService.logout_invalidateToken(userId); + + res.status(200).json({ message: "Logout successful" }); + } catch (error) { + console.error("Error during logout:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +// controller.js +exports.getAppUsersByHospitalCode = async (req, res) => { + try { + // Extract the hospital_code from the token (req.user object) + const userId = req.user.id; + + if (!userId) { + return res.status(400).json({ error: "User not found for the token" }); + } + + // Query to fetch hospital users based on the hospital_code + const result = await appUserService.getAppUsersByHospitalCode(userId); + + if (result.length === 0) { + return res + .status(404) + .json({ error: "No users found for the specified Token" }); + } + + const hospitalData = await appUserService.getAppUsersByHospitalCode_getHospitalByCode(result[0].hospital_code); + if(hospitalData[0].publicSignupEnabled){ + result[0].status = 'Active' + } + // Send the response with the users data + res.status(200).json({ + message: "Hospital users fetched successfully", + user: result[0], + }); + } catch (error) { + console.error("Error fetching hospital users:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.approveUserId = async (req, res) => { + try { + const appUserId = req.params.id; // App user ID to approve + const { role } = req.user; // Authenticated user's details + const { Action } = req.body; + + const { hospital_code } = req.user; + + if (!req.body.Action) { + return res.status(400).json({ error: "Action is required" }); + } + + if (!["Superadmin", "Admin", 8].includes(role)) { + return res.status(403).json({ error: "Unauthorized to approve IDs" }); + } + + const result = await appUserService.approveUserId_getUserByIdAndHospital(appUserId, hospital_code); + + if (result.length === 0) { + return res + .status(403) + .json({ error: "User does not belong to your hospital" }); + } + + // Approve the ID + if (Action == "Reject") { + await appUserService.approveUserId_updateStatus("Inactive", appUserId); + res.status(200).json({ message: "ID Rejected successfully" }); + } else if (Action == "Approve") { + await appUserService.approveUserId_updateStatus("Active", appUserId); + res.status(200).json({ message: "ID approved successfully" }); + } + } catch (error) { + console.error("Error approving ID:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.getAppUsers = async (req, res) => { + try { + const { role } = req.user; + // Ensure the user has the proper role + if (!["Superadmin", "Admin", 9, 8].includes(role)) { + return res.status(403).json({ error: "Unauthorized to view app users" }); + } + + // Query to fetch app users + const users = await appUserService.getAppUsers(); + + if (users.length === 0) { + return res + .status(404) + .json({ message: "No app users found for this hospital" }); + } + + res.status(200).json({ + message: "App users fetched successfully", + data: users, + }); + } catch (error) { + console.error("Error fetching app users:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.getAppUserByHospitalId = async (req, res) => { + try { + const { role } = req.user; + const { id } = req.params; // Extract user ID from request parameters + // Log authenticated user details + + // Ensure the user has the proper role + if (!["Superadmin", "Admin", 8, 9].includes(role)) { + return res.status(403).json({ error: "Unauthorized to view app users" }); + } + + // fetching hospital code from hospital id + const result1 = await appUserService.getAppUserByHospitalId_getHospitalById(id); + const hospital_code = result1[0].hospital_code; + + // Query to fetch the app user by hospital_code + const users = await appUserService.getAppUserByHospitalId_getUsersByHospitalCode(hospital_code); + + if (users.length === 0) { + return res.status(404).json({ message: "App users not found" }); + } + + res.status(200).json({ + message: "App user fetched successfully", + data: users, // Return single user object + }); + } catch (error) { + console.error("Error fetching app user:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.deleteAppUser = async (req, res) => { + try { + const { id } = req.params; + const { hospital_code } = req.user; + + const users = await appUserService.deleteAppUser_getUserById(id); + + if (users.length === 0) { + return res + .status(404) + .json({ message: "No app users found for this hospital" }); + } + + // Ensure only Admin or Superadmin can delete users + if (!["Admin", "Superadmin", 8, 7,"AppUser"].includes(req.user.role)) { + return res.status(403).json({ error: "Unauthorized to delete users" }); + } + + // Ensure user belongs to the same hospital + const result = await appUserService.deleteAppUser_getUserByIdAndHospital(id, hospital_code); + + if (!result || result.length === 0) { + return res + .status(403) + .json({ error: "User does not belong to your hospital" }); + } + + // Unlink (delete) the associated file if it exists + const filePath = path.join(__dirname, '..','..', 'uploads', result[0].id_photo_url.replace(/^\/uploads\//, '')); + + fs.access(filePath, fs.constants.F_OK, (err) => { + if (err) { + console.error(`File not found: ${filePath}`); + return; + } + + fs.unlink(filePath, (err) => { + if (err) { + console.error('Error deleting file:', err.message); + } else { + } + }); + }); + + // delete feedback + await appUserService.deleteAppUser_deleteFeedback(req.user.id); + + // Delete the user + const deleteResult = await appUserService.deleteAppUser_deleteUser(id); + + if (deleteResult.affectedRows === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.status(200).json({ message: "User deleted successfully" }); + } catch (error) { + console.error("Error deleting user:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +// query title CRUD +exports.updateQueryTitle = async (req, res) => { + const id = req.user.id; + const { query_title } = req.body; + + if (!query_title) { + return res.status(400).json({ error: "query_title is required" }); + } + + try { + const result = await appUserService.updateQueryTitle(id, query_title); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ message: "Query title updated successfully" }); + } catch (error) { + console.error("Error updating query title:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +// Delete query_title for a user by user ID + +exports.getShortTitle = (req, res) => { + const { question } = req.body; + + if (!question) return res.status(400).json({ error: "No question provided" }); + + let shortTitle = nlp(question).sentences().toTitleCase().out("text"); + + // Remove unnecessary question words + shortTitle = shortTitle + .replace( + /^(What|How|Why|When|Where|Which|Who|Is|Are|Do|Does|Can|Could)\s+/i, + "" + ) + .trim(); + + if (!shortTitle) + return res.status(400).json({ error: "Invalid question format" }); + + + res.json({ query_title: shortTitle }); +}; + +exports.deleteQueryTitle = async (req, res) => { + const id = req.user.id; + + try { + const result = await appUserService.deleteQueryTitle(id); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ message: "Query title deleted successfully" }); + } catch (error) { + console.error("Error deleting query title:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.sendOtp = async (req, res) => { + try { + const { email } = req.body; + + // Validate email input + if (!email) { + return res.status(400).json({ error: "Email is required" }); + } + + // Check if user exists + const user = await appUserService.sendOtp_getUserByEmail(email); + if (!user.length) { + return res.status(404).json({ error: "User not found" }); + } + + const userId = user[0].id; + const hospital_code = user[0].hospital_code; + + // fetch hospital name of app_user + const hospital = await appUserService.sendOtp_getHospitalNameByCode(hospital_code); + + // Generate a 6-digit OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000); // OTP expires in 1 hour + + // Store OTP in database + await appUserService.sendOtp_updateOtp(otp, expiresAt, userId); + + // // Send OTP via email + const info = await sendMail( + email, + hospital[0].name_hospital, + user[0].username, + otp + ); + + res.json({ message: "OTP sent successfully", email_status: info.response }); + } catch (error) { + console.error("Error sending OTP:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; +exports.changePassword = async (req, res) => { + try { + const { email, otp, new_password } = req.body; + + // Validate inputs + if (!email || !otp || !new_password) { + return res + .status(400) + .json({ error: "Email, OTP, and new password are required" }); + } + + // Fetch OTP from the database + const user = await appUserService.changePassword_getUserByEmail(email); + + if (!user.length) { + return res.status(404).json({ error: "User not found" }); + } + + const userData = user[0]; + + // ✅ Check if OTP matches + if (userData.otp_code !== otp) { + return res.status(400).json({ error: "Invalid OTP" }); + } + + // ✅ Check if OTP is expired + if (new Date() > new Date(userData.otp_expires_at)) { + return res.status(400).json({ error: "OTP expired. Request a new one." }); + } + + // ✅ Hash the new password + const hashedPassword = await bcrypt.hash(new_password, 10); + + // ✅ Update password in DB & clear OTP + await appUserService.changePassword_updatePassword(hashedPassword, userData.id); + + res.json({ message: "Password changed successfully!" }); + } catch (error) { + console.error("Error resetting password:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.sendPinOtp = async (req, res) => { + try { + const { email } = req.body; + + // Validate email input + if (!email) { + return res.status(400).json({ error: "Email is required" }); + } + + // Check if user exists + const user = await appUserService.sendPinOtp_getUserByEmail(email); + if (!user.length) { + return res.status(404).json({ error: "User not found" }); + } + + const userId = user[0].id; + const hospital_code = user[0].hospital_code; + + // fetch hospital name of app_user + const hospital = await appUserService.sendPinOtp_getHospitalNameByCode(hospital_code); + + // Generate a 6-digit OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000); // OTP expires in 1 hour + + // Store OTP in database + await appUserService.sendPinOtp_updatePinOtp(otp, expiresAt, userId); + + // // Send OTP via email + const info = await sendMail( + email, + hospital[0].name_hospital, + user[0].username, + otp + ); + + res.json({ message: "OTP sent successfully", email_status: info.response }); + } catch (error) { + console.error("Error sending OTP:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.changePinByOtp = async (req, res) => { + try { + const { email, otp, new_pin } = req.body; + + // Validate inputs + if (!email || !otp || !new_pin) { + return res + .status(400) + .json({ error: "Email, OTP, and new pin are required" }); + } + + // Fetch OTP from the database + const user = await appUserService.changePinByOtp_getUserByEmail(email); + + if (!user.length) { + return res.status(404).json({ error: "User not found" }); + } + + const userData = user[0]; + + // ✅ Check if OTP matches + if (userData.pin_otp !== otp) { + return res.status(400).json({ error: "Invalid OTP" }); + } + + // ✅ Check if OTP is expired + if (new Date() > new Date(userData.otp_expires_at)) { + return res.status(400).json({ error: "OTP expired. Request a new one." }); + } + + // ✅ Hash the new password + + // ✅ Update password in DB & clear OTP + await appUserService.changePinByOtp_updatePin(new_pin, userData.id); + + res.json({ message: "Pin changed successfully!" }); + } catch (error) { + console.error("Error resetting pin:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +async function sendMail(email, hospital_name, username, otp) { + const htmlContent = generatePasswordResetEmail(hospital_name, username, otp); + + const mailOptions = { + from: process.env.EMAIL_USER, // Sender's email + to: email, // Recipient's email + subject: "Spurrinai Login Credentials", // Email subject + html: htmlContent, + }; + + try { + const info = await transporter.sendMail(mailOptions); + return info; + } catch (error) { + console.error(`Error sending email to ${email}:`, error); + return error; + } +} + +// chat-sessions +exports.getChatSessionsByAppUserID = async (req, res) => { + try { + + if (!req.user.id) { + return res.status(400).json({ error: "user id not found" }); + } + // Query to fetch app users + const session_ids = await appUserService.getChatSessionsByAppUserID(req.user.id); + + if (session_ids.length === 0) { + return res.status(404).json({ message: 'No interaction logs found for this app user', data: session_ids }); + } + + res.status(200).json({ + message: 'Interaction logs fetched successfully', + data: session_ids, + }); + } catch (error) { + console.error('Error fetching interaction logs:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } +} + +exports.getChatForEachSession = async (req, res) => { + try { + + const session_id = req.params.session_id; + + if (!req.user.id) { + return res.status(400).json({ error: "user id not found" }); + } + // Query to fetch app users + const interaction_logs = await appUserService.getChatForEachSession(req.user.id, session_id); + + if (interaction_logs.length === 0) { + return res.status(404).json({ message: 'No interaction logs found for this app user or session', data: interaction_logs }); + } + + res.status(200).json({ + message: 'Interaction logs fetched successfully', + data: interaction_logs, + }); + } catch (error) { + console.error('Error fetching interaction logs:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } +} + +exports.deleteChatSessions = async (req, res) => { + const id = req.user.id; + const { session_id } = req.body + + try { + const result = await appUserService.deleteChatSessions(id, session_id); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: "No session found" }); + } + + res.json({ message: "Chat session deleted successfully" }); + } catch (error) { + console.error("Error deleting query title:", error); + res.status(500).json({ error: "Internal server error" }); + } +} + +exports.clearChatbasedOnSessions = async (req, res) => { + const id = req.user.id; + const { session_id } = req.body + + try { + const result = await appUserService.clearChatbasedOnSessions(id, session_id); + + if (result.affectedRows === 0) { + return res.status(404).json({ error: "No chat found" }); + } + + res.json({ message: "Chat deleted successfully" }); + } catch (error) { + console.error("Error deleting chat:", error); + res.status(500).json({ error: "Internal server error" }); + } +} + +exports.getChatByTime = async (req, res) => { + try { + const { lastcreated_at } = req.body; + + if (!req.user.id) { + return res.status(400).json({ error: "user id not found" }); + } + // Query to fetch app users + + let results = await appUserService.getChatByTime(req.user.id, lastcreated_at); + + if (results.length === 0) { + return res.status(404).json({ message: 'No interaction logs found for this app user' }); + } + + res.status(200).json({ + message: 'Interaction logs fetched successfully', + data: results, + }); + } catch (error) { + console.error('Error fetching interaction logs:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } + +} + +exports.checkEmailCode = async (req, res) => { + try { + const { email, hospital_code } = req.body; + + // Check if the email and code are provided + if (!email || !hospital_code) { + return res.status(400).json({ error: "Email and code are required" }); + } + + // Check if the email exists in the database + const useremail = await appUserService.checkEmailCode_userByEmail(email); + if (useremail.length) { + return res.status(404).json({ error: "Email already in use" }); + } + + // Check if the code is correct + const usercode = await appUserService.checkEmailCode_userByCode(hospital_code); + + if (!usercode.length) { + return res.status(404).json({ error: "Please enter valid hospital code" }); + } + + // Code is correct, return success response + res.status(200).json({ message: "Email code is correct" }); + } catch (error) { + console.error("Error checking email code:", error); + res.status(500).json({ error: "Internal server error" }); + } +} + +// get popular topics +exports.getPopularTopics = async (req, res) => { + let popularTopics = []; + try { + // Fetch top 4 most viewed questions + const rows = await appUserService.getMappedPopularQuestionsAnswers(req.user.hospital_code); + if (!rows) { + popularTopics = [] + } + else { + popularTopics = rows; + } + res.status(200).json({ + success: true, + data: popularTopics + }); + } catch (error) { + console.error('Error fetching popular topics:', error); + res.status(500).json({ + success: false, + message: 'Internal server error' + }); + } +} + +exports.changePin = async (req, res) => { + try { + const { current_pin, new_pin } = req.body; + const userId = req.user.id; + + if (!current_pin || !new_pin) { + return res.status(400).json({ error: "Current pin and new pin are required" }); + } + + // Add PIN length validation + if (new_pin.length !== 4 || !/^\d+$/.test(new_pin)) { + return res.status(400).json({ error: "New PIN must be exactly 4 digits" }); + } + + // Get user's current pin + const user = await appUserService.changePin_getUserById(userId); + + if (!user.length) { + return res.status(404).json({ error: "User not found" }); + } + + // Verify current pin + if (user[0].pin_number !== current_pin) { + return res.status(400).json({ error: "Current pin is incorrect" }); + } + + // Check if new pin is same as current pin + if (current_pin === new_pin) { + return res.status(400).json({ error: "New PIN cannot be the same as current PIN" }); + } + + // Update to new pin + await appUserService.changePin_updatePin(new_pin, userId); + + res.json({ message: "Pin changed successfully" }); + } catch (error) { + console.error("Error changing pin:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.forgotPin = async (req, res) => { + try { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ error: "Email is required" }); + } + + // Get user details + const user = await appUserService.forgotPin_getUserByEmail(email); + + if (!user.length) { + return res.status(404).json({ error: "User not found" }); + } + + const userData = user[0]; + const htmlContent = generatePasswordResetEmail(userData.name_hospital, userData.username, userData.pin_number); + + // Send pin via email + const mailOptions = { + from: process.env.EMAIL_USER, + to: email, + subject: "Your Spurrinai PIN", + html: htmlContent + }; + + let info + try { + info = await transporter.sendMail(mailOptions); + } catch (emailError) { + console.error("Email sending failed:", emailError.message); + info = "Email sending failed: " + emailError.message; + } + res.json({ message: "PIN has been generated", emailInfo : info.response }); + } catch (error) { + console.error("Error in forgot pin:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; \ No newline at end of file diff --git a/src/controllers/authController.js b/src/controllers/authController.js new file mode 100644 index 0000000..d0be9af --- /dev/null +++ b/src/controllers/authController.js @@ -0,0 +1,99 @@ +const authService = require('../services/authService'); + +exports.logout = async (req, res) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.status(401).json({ error: "Access token required" }); + } + + try { + const result = await authService.logout(token); + res.status(200).json(result); + } catch (error) { + console.error("Error during logout:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.refreshToken = async (req, res) => { + const { refreshToken, user_id } = req.body; + + if (!refreshToken || !user_id) { + return res.status(400).json({ error: "Refresh token and user ID are required" }); + } + + try { + const result = await authService.refreshToken(refreshToken, user_id); + res.status(200).json(result); + } catch (error) { + console.error("Error generating access token:", error.message); + res.status(403).json({ error: "Invalid or expired refresh token" }); + } +}; + +exports.login = async (req, res) => { + const authHeader = req.headers["authorization"]; + const providedAccessToken = authHeader && authHeader.split(" ")[1]; + + if (!providedAccessToken) { + return res.status(401).json({ error: "Authorization token is required" }); + } + + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: "Email and password are required" }); + } + + try { + const response = await authService.login(providedAccessToken, email, password); + res.status(200).json(response); + } catch (error) { + console.error("Error during login:", error.message); + if (error.message.includes("Invalid") || error.message.includes("expired")) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes("password")) { + return res.status(401).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.authenticateToken = async (req, res, next) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.status(401).json({ error: "Access token required" }); + } + + try { + const hospital_id = parseInt(req.params.hospital_id, 10); + const user = await authService.authenticateToken(token, hospital_id); + req.user = user; + next(); + } catch (error) { + console.error("Token verification error:", error.message); + res.status(403).json({ error: error.message }); + } +}; + +exports.checkAccessToken = async (req, res) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.status(401).json({ error: "Access token required" }); + } + + try { + const result = await authService.checkAccessToken(token); + res.status(200).json(result); + } catch (error) { + console.error("Error during token check:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; \ No newline at end of file diff --git a/src/controllers/documentsController.js b/src/controllers/documentsController.js new file mode 100644 index 0000000..9e7dba3 --- /dev/null +++ b/src/controllers/documentsController.js @@ -0,0 +1,437 @@ +const db = require('../config/database'); +const axios = require('axios'); +const fs = require('fs'); +const FormData = require('form-data'); // Ensure this is imported correctly +const path = require('path') +let queue = []; // Job queue +let failedQueue = []; +let isProcessing = false; // Flag to track processing state +const CHECK_INTERVAL = 60000; // Interval to check status update (60 sec) +const checkDocumentStatus = async (documentId) => { + try { + const [rows] = await db.query( + 'SELECT processed_status, failed_page FROM documents WHERE id = ?', + [documentId] + ); + + if (rows && rows.processed_status) { + const processed_status = rows.processed_status; + const failed_page = rows.failed_page; + + return { processed_status, failed_page }; + } else { + return { processed_status: null, failed_page: null }; + } + } catch (error) { + console.error(`Error checking document status: ${error.message}`); + return { processed_status: null, failed_page: null }; + } +}; + +exports.repopulateQueueOnStartup = async () => { + try { + console.log("Checking documents on startup..."); + + // Query documents with 'Pending' or 'Failed' status + const result = await db.query( + 'SELECT id, file_url, hospital_id, failed_page FROM documents WHERE processed_status IN (?, ?)', + ['Pending', 'Failed'] + ); + + + if (Array.isArray(result) && result.length > 0) { + result.forEach(doc => { + if (!doc.file_url || typeof doc.file_url !== "string" || doc.file_url.trim() === "") { + console.warn(`⚠️ Skipping document ${doc.id}: Invalid or missing file_url`); + return; // Skip documents with invalid file_url + } + + queue.push({ + file: { + path: String(doc.file_url).trim(), // Ensure it's a valid string + name: path.basename(doc.file_url) // Extract file name safely + }, + hospital_id: doc.hospital_id, + documentId: doc.id, + failed_page: doc.failed_page + }); + }); + } + + // console.log("✅ Documents added to queue:", queue); + + // Start processing if the queue is not empty + if (queue.length > 0 && !isProcessing) { + processQueue(); + } + } catch (error) { + console.error('Error repopulating queue on startup:', error.message); + } +}; + +// Function to process the queue +const RETRY_DELAY = 5000; // 5 seconds +const RETRY_LIMIT = 3; +const retryMap = new Map(); // To track retry counts per document + +// Implementation Steps +// Add new PDFs to the queue with Pending status. +// Start processing only if no job is currently active. +// Wait for Python API to update processed_status in the database. +// Check database periodically for status change. +// Once status is updated, process the next PDF. + +const processQueue = async () => { + if (isProcessing) return; // If already processing, don't start again + isProcessing = true; + + // Start the queue processing + while (queue.length > 0 || failedQueue.length > 0) { + // Check if there are jobs in the queue (either main or failed queue) + if (queue.length === 0 && failedQueue.length === 0) { + console.log("Queue is empty. Waiting for jobs..."); + await new Promise(resolve => { + const checkForNewJobs = setInterval(() => { + if (queue.length > 0 || failedQueue.length > 0) { + clearInterval(checkForNewJobs); + resolve(); // Resume once a new job is added + } + }, 1000); // Check for new jobs every second + }); + } + + // Move jobs from failed queue to main queue if necessary + if (queue.length === 0 && failedQueue.length > 0) { + console.log("Switching to failed queue..."); + queue.push(...failedQueue); + failedQueue.length = 0; + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); // Delay before retrying + } + + // If there are jobs, process the next one + if (queue.length > 0) { + console.log("the queue is :", queue); + const job = queue.shift(); + let filePath = path.resolve(__dirname, '..','..', 'uploads', 'documents', path.basename(job.file.path)); + if (!fs.existsSync(filePath)) { + console.error(`File not found: "${filePath}". Removing from queue.`); + + // Clean up retry tracking + retryMap.delete(job.documentId); + + // Remove from queue + queue = queue.filter(item => item.documentId !== job.documentId); + // failedQueue = failedQueue.filter(item => item.documentId !== job.documentId); + // const job = queue.shift(); + + continue; // Skip to next job + } + + filePath = path.resolve(__dirname, '..','..', 'uploads', 'documents', path.basename(job.file.path)); + + console.log(`Processing document: ${job.file.path}`); + + await db.query('UPDATE documents SET processed_status = ? WHERE id = ?', ['Pending', job.documentId]); + // const filePath = job.file.path.trim(); + + console.log("🔍 Checking file at:", filePath); + + if (!fs.existsSync(filePath)) { + console.error(`File not found: "${filePath}"`); + return; // Stop execution if the file does not exist + } + + // Ensure filePath is valid before using fs.createReadStream + const formData = new FormData(); + + try { + + + const fileStream = fs.createReadStream(filePath); + formData.append('pdf', fileStream); // Ensure fileStream is valid + formData.append('doc_id', job.documentId); + formData.append('hospital_id', job.hospital_id); + formData.append('failed_page', job.failed_page); + + + + } catch (error) { + // console.error(" Error creating read stream:", error.message); + } + try { + await axios.post(process.env.FLASK_BASE_URL+'flask-api/process-pdf', formData, { + headers: formData.getHeaders(), + }); + + console.log(`Python API called for ${job.file.path}`); + + // Poll the status of the document until it is processed or fails + const pollStatus = async () => { + const statusData = await checkDocumentStatus(job.documentId); + if (statusData.processed_status === 'Processed') { + console.log(`Document ${job.file.path} marked as ${statusData.processed_status}.`); + retryMap.delete(job.documentId); // Clear retry count + + } else if (statusData.processed_status === 'Failed') { + const newRetry = (retryMap.get(job.documentId) || 0) + 1; + retryMap.set(job.documentId, newRetry); + if (newRetry >= RETRY_LIMIT) { + console.warn(` Document ${job.file.path} failed ${newRetry} times. Removing from all queues.`); + retryMap.delete(job.documentId); + console.log("prompting user ---- ") + await db.query( + 'UPDATE documents SET reason = ? WHERE id = ?', + ['This PDF could not be processed due to access restrictions.', job.documentId] + ); + console.log("prompted user---- ") + + queue = queue.filter(item => item.documentId !== job.documentId); + failedQueue = failedQueue.filter(item => item.documentId !== job.documentId); + } else { + console.log(`Retrying (${newRetry}/${RETRY_LIMIT}) for ${job.file.path}`); + // fetch freshly + const statusdata = await checkDocumentStatus(job.documentId); + + failedQueue.push({ file: {path:statusdata.file_url}, hospital_id: statusdata.hospital_id, documentId: statusdata.id, failed_page: statusdata.failed_page }); + // queue.push({ ...job }); + } + console.log(`Document ${job.file.path} failed. Adding back to failedQueue.`); + } else { + + if (queue.length === 0 && failedQueue.length === 0) { + console.log(" Queue is empty during polling. Stopping poll."); + return; + } + setTimeout(pollStatus, CHECK_INTERVAL); + } + }; + + await pollStatus(); + + } catch (error) { + console.error(`Error processing document ${job.file.path}: ${error.message}`); + const newRetry = (retryMap.get(job.documentId) || 0) + 1; + retryMap.set(job.documentId, newRetry); + console.warn(` Document ${job.file.path} failed ${newRetry} times.`); + const statusdata = await checkDocumentStatus(job.documentId); + + console.log('statusdata------',statusdata) + if (newRetry >= RETRY_LIMIT) { + console.warn(`Skipping ${job.file.path} after ${newRetry} failed attempts. Removing from all queues.`); + retryMap.delete(job.documentId); + await db.query( + 'UPDATE documents SET reason = ? WHERE id = ?', + ['This PDF could not be processed due to access restrictions.', job.documentId] + ); + queue = queue.filter(item => item.documentId !== job.documentId); + failedQueue = failedQueue.filter(item => item.documentId !== job.documentId); + } else { + job.failed_page = statusdata.failed_page; + + failedQueue.push(job); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); + } + } + } + } + + console.log("All jobs processed."); + isProcessing = false; +}; +// Function to add a document to the queue +const processDocumentFromPy = async (file, hospital_id, documentId, failed_page) => { + queue.push({ file, hospital_id, documentId, failed_page }); + // console.log(`Added to queue: ${file.path}`); + + if (!isProcessing) { + processQueue(); // Start processing if idle + } +}; + +exports.uploadDocument = async (req, res) => { + try { + // const { hospital_id } = req.body; + const hospital_id = req.user.hospital_id; + const uploaded_by = req.user.id; + const file_name = req.file.originalname; + const file_url = `/uploads/documents/${req.file.filename}`; + const failed_page = req.body.failed_page; + + console.log("req.user----",req.user) + if (!["Superadmin","Admin",7,8].includes(req.user.role)) { + return res + .status(403) + .json({ error: "You are not authorized to upload documents" }); + } + // Step 1: Insert document details into the `documents` table + const insertQuery = ` + INSERT INTO documents (hospital_id, uploaded_by, file_name, file_url, processed_status) + VALUES (?, ?, ?, ?, 'Pending') + `; + const result = await db.query(insertQuery, [hospital_id, uploaded_by, file_name, file_url]); + const documentId = result.insertId; + + processDocumentFromPy(req.file, hospital_id, documentId, failed_page) + + if (result || result.affectedRows > 0) { + res.status(200).json({ + message: 'Document uploaded!', + }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +exports.getDocumentsByHospital = async (req, res) => { + try { + const { hospital_id } = req.params; + + // Ensure the authenticated user is either Admin or Superadmin + if (!['Admin', 'Superadmin', 'Viewer', 8, 9, 7].includes(req.user.role)) { + return res.status(403).json({ error: 'You are not authorized to view documents' }); + } + + // Ensure the user belongs to the correct hospital + if (req.user.hospital_id !== parseInt(hospital_id, 10)) { + return res.status(403).json({ error: 'You are not authorized to access documents for this hospital' }); + } + + // Fetch documents + const documents = await db.query('SELECT * FROM documents WHERE hospital_id = ?', [hospital_id]); + + res.status(200).json({ documents }); + } catch (error) { + // console.error('Error fetching documents:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.updateDocumentStatus = async (req, res) => { + try { + const { id } = req.params; + const { processed_status } = req.body; + + // Fetch the document to validate ownership + const documentQuery = 'SELECT hospital_id FROM documents WHERE id = ?'; + const documentResult = await db.query(documentQuery, [id]); + + if (documentResult.length === 0) { + return res.status(404).json({ error: 'Document not found' }); + } + + const document = documentResult[0]; + + // Ensure the authenticated user is either Admin or Superadmin + if (!['Admin', 'Superadmin', 8, 7].includes(req.user.role)) { + return res.status(403).json({ error: 'You are not authorized to update documents' }); + } + + // Ensure the user belongs to the same hospital as the document + if (req.user.hospital_id !== document.hospital_id) { + return res.status(403).json({ error: 'You are not authorized to update documents for this hospital' }); + } + + // Update document status + const updateQuery = 'UPDATE documents SET processed_status = ? WHERE id = ?'; + const result = await db.query(updateQuery, [processed_status, id]); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Document not found or no changes made' }); + } + + res.status(200).json({ message: 'Document status updated successfully!' }); + } catch (error) { + console.error('Error updating document status:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.deleteDocument = async (req, res) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ error: 'Document ID is required' }); + } + + // Fetch the document to validate ownership + const documentQuery = 'SELECT * FROM documents WHERE id = ?'; + const documentResult = await db.query(documentQuery, [id]); + + if (documentResult.length === 0) { + return res.status(404).json({ error: 'Document not found' }); + } + + const document = documentResult[0]; + + // Authorization check + if (!['Admin', 'Superadmin', 8, 7].includes(req.user.role)) { + return res.status(403).json({ error: 'You are not authorized to delete documents' }); + } + + if (req.user.hospital_id !== document.hospital_id) { + return res.status(403).json({ error: 'You are not authorized to delete documents for this hospital' }); + } + + // 🔁 Make a call to Flask API to delete vectors + try { + const flaskResponse = await axios.delete(process.env.FLASK_BASE_URL + 'flask-api/delete-document-vectors', { + data: { + hospital_id: document.hospital_id, + doc_id: document.id + } + }); + + if (flaskResponse.status !== 200) { + return res.status(flaskResponse.status).json(flaskResponse.data); + } + } catch (flaskError) { + console.error('Flask API error:', flaskError.message); + const errorData = flaskError.response?.data || { error: 'Failed to delete document vectors' }; + return res.status(500).json(errorData); + } + + // Delete dependent records + try { + await Promise.all([ + db.query('DELETE FROM questions_answers WHERE document_id = ?', [id]), + db.query('DELETE FROM document_metadata WHERE document_id = ?', [id]) + ]); + } catch (error) { + console.error("Error deleting dependent records:", error.message); + return res.status(500).json({ error: "Failed to delete dependent records" }); + } + + // Delete file if it exists + const filePath = path.join(__dirname, '..','..', 'uploads', document.file_url.replace(/^\/uploads\//, '')); + + fs.access(filePath, fs.constants.F_OK, (err) => { + if (err) { + console.warn(`File not found: ${filePath}`); + } else { + fs.unlink(filePath, (err) => { + if (err) { + console.error('Error deleting file:', err.message); + } else { + console.log('File deleted successfully:', filePath); + } + }); + } + }); + + // Finally, delete the document + const deleteQuery = 'DELETE FROM documents WHERE id = ?'; + const result = await db.query(deleteQuery, [id]); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Document not found' }); + } + + res.status(200).json({ message: 'Document deleted successfully!' }); + + } catch (error) { + console.error('Error deleting document:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } +}; \ No newline at end of file diff --git a/src/controllers/exceldataController.js b/src/controllers/exceldataController.js new file mode 100644 index 0000000..a24907c --- /dev/null +++ b/src/controllers/exceldataController.js @@ -0,0 +1,30 @@ +const exceldataService = require('../services/exceldataService'); + +// Create a new record +exports.createExcelEntry = async (req, res) => { + try { + const requestorRole = req.user.role; + const { hospital_id, hospital_code } = req.user; + + const result = await exceldataService.createExcelEntry( + hospital_id, + hospital_code, + requestorRole, + req.body + ); + + res.status(201).json(result); + } catch (error) { + console.error("Error inserting data:", error.message); + if (error.message.includes('Access denied')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('Invalid data format')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Hospital not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: error.message }); + } +}; \ No newline at end of file diff --git a/src/controllers/feedbacksController.js b/src/controllers/feedbacksController.js new file mode 100644 index 0000000..c31515d --- /dev/null +++ b/src/controllers/feedbacksController.js @@ -0,0 +1,119 @@ +const feedbacksService = require('../services/feedbacksService'); + +// Create feedback from app user to hospital +exports.createAppUserFeedback = async (req, res) => { + try { + const user_id = req.user.id; + const { hospital_code } = req.body; + + const result = await feedbacksService.createAppUserFeedback(user_id, hospital_code, req.body); + res.status(201).json(result); + } catch (error) { + console.error('Error creating app user feedback:', error); + if (error.message.includes('Hospital code is required')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Hospital not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +// Create feedback from hospital to Spurrin +exports.createHospitalFeedback = async (req, res) => { + try { + const hospital_code = req.user.hospital_code; + const result = await feedbacksService.createHospitalFeedback(hospital_code, req.body); + res.status(201).json(result); + } catch (error) { + console.error("Error creating hospital feedback:", error); + if (error.message.includes('required') || error.message.includes('Invalid')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Hospital not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +// Get feedbacks for a hospital (for hospital users) +exports.getHospitalFeedbacks = async (req, res) => { + try { + const hospital_code = req.user.hospital_code; + const result = await feedbacksService.getHospitalFeedbacks(hospital_code); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching hospital feedbacks:", error); + if (error.message.includes('Hospital not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +// Get all feedbacks (for Spurrin admin) +exports.getAllFeedbacks = async (req, res) => { + try { + const result = await feedbacksService.getAllFeedbacks(req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching all feedbacks:", error); + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +// Forward app user feedbacks to Spurrin (for hospital users) +exports.forwardAppUserFeedbacks = async (req, res) => { + try { + const hospital_code = req.user.hospital_code; + const { feedback_ids } = req.body; + + const result = await feedbacksService.forwardAppUserFeedbacks(hospital_code, feedback_ids); + res.status(200).json(result); + } catch (error) { + console.error("Error forwarding feedbacks:", error); + if (error.message.includes('required') || error.message.includes('invalid')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Hospital not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +// API to get all forwarded feedbacks for Spurrin +exports.getForwardedFeedbacks = async (req, res) => { + try { + const result = await feedbacksService.getForwardedFeedbacks(req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching forwarded feedbacks:", error); + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.deleteAppUserFeedback = async (req, res) => { + try { + const feedbackId = req.params.id; + const result = await feedbacksService.deleteAppUserFeedback(feedbackId, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error('Error deleting app user feedback:', error); + if (error.message.includes('required')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; \ No newline at end of file diff --git a/src/controllers/hospitalController.js b/src/controllers/hospitalController.js new file mode 100644 index 0000000..5dfa01b --- /dev/null +++ b/src/controllers/hospitalController.js @@ -0,0 +1,315 @@ + +const hospitalService = require('../services/hospitalService'); + + +exports.createHospital = async (req, res) => { + try { + const result = await hospitalService.createHospital(req.body, req.body.super_admin_id, req.headers.authorization.split(" ")[1]); + res.status(201).json(result); + } catch (error) { + console.error("Error creating hospital and admin:", error.message); + if (error.message.includes("already exists")) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes("Invalid")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("Unauthorized")) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: error.message }); + } +}; + +exports.uploadLogo = (req, res) => { + try { + const logoUrl = req.file ? req.file.path : null; + res.status(200).json({ message: "Logo uploaded successfully!", logo_url: logoUrl }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +exports.getHospitalList = async (req, res) => { + if (!["Spurrinadmin", 6].includes(req.user.role)) { + return res.status(403).json({ error: "You are not authorized!" }); + } + try { + const result = await hospitalService.getHospitalList(req.user.id); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching hospital list:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.getHospitalById = async (req, res) => { + try { + const result = await hospitalService.getHospitalById(req.params.id); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching hospital by ID:", error.message); + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.updateHospital = async (req, res) => { + try { + const result = await hospitalService.updateHospital(req.params.id, req.body, req.user.id, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error updating hospital:", error.message); + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes("Invalid field")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("You can only edit")) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.deleteHospital = async (req, res) => { + try { + const result = await hospitalService.deleteHospital(req.params.id, req.user.id, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error deleting hospital:", error.message); + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes("not authorized") || error.message.includes("You can only delete")) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.getAllHospitalUsers = async (req, res) => { + try { + const result = await hospitalService.getAllHospitalUsers(); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching hospital users:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.getColorsFromHospital = async (req, res) => { + try { + const result = await hospitalService.getColorsFromHospital(req.user.id, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching hospital:", error.message); + if (error.message.includes("not authorized")) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.changePassword = async (req, res) => { + try { + const result = await hospitalService.changePassword(req.user.id, req.body.new_password, req.headers.authorization); + res.status(200).json(result); + } catch (error) { + console.error("Error updating password:", error.message); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("Invalid") || error.message.includes("expired")) { + return res.status(401).json({ error: error.message }); + } + if (error.message.includes("not match")) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.sendTempPassword = async (req, res) => { + try { + const result = await hospitalService.sendTempPassword(req.body.email); + res.json(result); + } catch (error) { + console.error("Error sending OTP:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.changeTempPassword = async (req, res) => { + try { + const result = await hospitalService.changeTempPassword(req.body.email, req.body.temp_password, req.body.new_password); + res.json(result); + } catch (error) { + console.error("Error resetting password:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes("Invalid") || error.message.includes("expired")) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.updateHospitalName = async (req, res) => { + try { + const result = await hospitalService.updateHospitalName(req.user.id, req.body.hospital_name); + res.json(result); + } catch (error) { + console.error("Error updating hospital name:", error.message); + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.sendTemporaryPassword = async (req, res) => { + try { + const result = await hospitalService.sendTemporaryPassword(req.body.email); + res.json(result); + } catch (error) { + console.error("Error sending OTP:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.changeTempPasswordAdminsViewers = async (req, res) => { + try { + const result = await hospitalService.changeTempPasswordAdminsViewers(req.body.email, req.body.temp_password, req.body.new_password); + res.json(result); + } catch (error) { + console.error("Error resetting password:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes("Invalid") || error.message.includes("expired")) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.checkNewAppUser = async (req, res) => { + if (!["Superadmin", "Admin", 7, 8].includes(req.user.role)) { + return res.status(403).json({ error: "You are not authorized" }); + } + + try { + const result = await hospitalService.checkNewAppUser(req.body.hospital_code); + res.json(result); + } catch (error) { + console.error("Error checking new notification:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.updateAppUserChecked = async (req, res) => { + if (!["Superadmin", "Admin", 7, 8].includes(req.user.role)) { + return res.status(403).json({ error: "You are not authorized" }); + } + + try { + const result = await hospitalService.updateAppUserChecked(req.params.id); + res.json(result); + } catch (error) { + console.error("Error updating checked status:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.interactionLogs = async (req, res) => { + if (!["Superadmin", 7].includes(req.user.role)) { + return res.status(403).json({ error: "You are not authorized" }); + } + + try { + const result = await hospitalService.interactionLogs(req.body.hospital_code, req.body.app_user_id); + res.json(result); + } catch (error) { + console.error("Error fetching logs:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.updatePublicSignup = async (req, res) => { + try { + const result = await hospitalService.updatePublicSignup(req.params.id, req.body.enabled, req.user.id, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error updating public signup setting:", error); + if (error.message.includes("Invalid input")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not authorized") || error.message.includes("your own hospital")) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.getPublicSignup = async (req, res) => { + try { + const result = await hospitalService.getPublicSignup(req.params.id, req.user.id, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error updating public signup setting:", error); + if (error.message.includes("not authorized") || error.message.includes("your own hospital")) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; \ No newline at end of file diff --git a/src/controllers/onboardingController.js b/src/controllers/onboardingController.js new file mode 100644 index 0000000..957a06c --- /dev/null +++ b/src/controllers/onboardingController.js @@ -0,0 +1,59 @@ +const onboardingService = require('../services/onboardingService'); + +exports.getOnboardingSteps = async (req, res) => { + try { + const { userId } = req.params; + const result = await onboardingService.getOnboardingSteps(userId, req.user.role, req.user.id); + res.status(200).json(result); + } catch (error) { + console.error('Error fetching onboarding steps:', error.message); + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.addOnboardingStep = async (req, res) => { + try { + const { userId, step } = req.body; + const result = await onboardingService.addOnboardingStep(userId, step, req.user.role, req.user.hospital_id); + res.status(201).json(result); + } catch (error) { + console.error('Error adding onboarding step:', error.message); + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes('Failed to add') || error.message.includes('No changes made')) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.updateOnboardingStep = async (req, res) => { + try { + const { user_id } = req.params; + const { step } = req.body; + const result = await onboardingService.updateOnboardingStep(user_id, step, req.user.role, req.user.id); + res.status(200).json(result); + } catch (error) { + console.error('Error updating onboarding step:', error.message); + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes('No changes made')) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; \ No newline at end of file diff --git a/src/controllers/roleController.js b/src/controllers/roleController.js new file mode 100644 index 0000000..134d67f --- /dev/null +++ b/src/controllers/roleController.js @@ -0,0 +1,10 @@ +const roleService = require('../services/roleService'); + +exports.getAllRoles = async (req, res) => { + try { + const roles = await roleService.getAllRoles(); + res.json(roles); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; \ No newline at end of file diff --git a/src/controllers/superAdminController.js b/src/controllers/superAdminController.js new file mode 100644 index 0000000..84299c2 --- /dev/null +++ b/src/controllers/superAdminController.js @@ -0,0 +1,148 @@ +const superAdminService = require('../services/superAdminService'); +//sign up call for Spurrinadmin +exports.initializeSuperAdmin = async (req, res) => { + try { + const { email, password } = req.body; + + const result = await superAdminService.initializeSuperAdmin(email, password); + + res.status(201).json({ + message: 'SuperAdmin created successfully', + user: { + id: result.id, + email: result.email, + role: result.role, + }, + accessToken: result.accessToken, + refreshToken: result.refreshToken, + }); + } catch (error) { + console.error('Error initializing SuperAdmin:', error.message); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("already exists")) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.getAllSuperAdmins = async (req, res) => { + const authHeader = req.headers['authorization']; + const accessToken = authHeader && authHeader.split(' ')[1]; + + try { + const superAdmins = await superAdminService.getAllSuperAdmins(accessToken); + res.status(200).json(superAdmins); + } catch (error) { + console.error('Error fetching SuperAdmins:', error.message); + if (error.message.includes('Access token required')) { + return res.status(401).json({ error: error.message }); + } + if (error.message.includes('Unauthorized') || error.message.includes('Invalid or expired')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.addSuperAdmin = async (req, res) => { + try { + const { email, password } = req.body; + + const result = await superAdminService.addSuperAdmin(email, password); + + res.status(201).json({ + message: 'SuperAdmin added successfully', + user: { + id: result.id, + email: result.email, + role: result.role, + }, + accessToken: result.accessToken, + refreshToken: result.refreshToken, + }); + } catch (error) { + console.error('Error adding SuperAdmin:', error.message); + if (error.message.includes("already exists")) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.deleteSuperAdmin = async (req, res) => { + try { + await superAdminService.deleteSuperAdmin(req.params.id); + res.status(200).json({ message: 'Super admin deleted successfully' }); + } catch (error) { + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: error.message }); + } +}; + +exports.sendTempPassword = async (req, res) => { + try { + const { email } = req.body; + const result = await superAdminService.sendTempPassword(email); + res.json(result); + } catch (error) { + console.error("Error sending temporary password:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: error.message }); + } +}; + +exports.changeTempPassword = async (req, res) => { + try { + const { email, temp_password, new_password } = req.body; + const result = await superAdminService.changeTempPassword(email, temp_password, new_password); + res.json(result); + } catch (error) { + console.error("Error resetting password:", error); + if (error.message.includes("required")) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes("not found")) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes("Invalid") || error.message.includes("expired")) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; + +exports.getDataConsumptionReport = async (req, res) => { + try { + const report = await superAdminService.getDataConsumptionReport(req.user.role); + res.status(200).json(report); + } catch (error) { + console.error('Error generating data consumption report:', error); + if (error.message.includes("not authorized")) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.getOnboardedHospitals = async (req, res) => { + try { + const result = await superAdminService.getOnboardedHospitals(req.user.role); + res.status(200).json(result); + } catch (error) { + console.error("Error fetching onboarded hospitals:", error); + if (error.message.includes("not authorized")) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } +}; \ No newline at end of file diff --git a/src/controllers/userController.js b/src/controllers/userController.js new file mode 100644 index 0000000..cb2af1e --- /dev/null +++ b/src/controllers/userController.js @@ -0,0 +1,260 @@ +const multer = require('multer'); +const path = require('path'); +const userService = require('../services/userService'); + + +exports.addUser = async (req, res) => { + try { + const { hospital_id, role_id, ...rest } = req.body; + const result = await userService.addUser(hospital_id, role_id, { ...rest, password: req.body.password }, req.user.role, req.user.hospital_id); + res.status(201).json(result); + } catch (error) { + console.error('Error adding user:', error.message); + if (error.message.includes('Access denied') || error.message.includes('Email already exists')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: error.message }); + } +}; + +exports.getUsersByHospital = async (req, res) => { + try { + const hospital_id = parseInt(req.params.hospital_id, 10); + const result = await userService.getUsersByHospital(hospital_id, req.user.role, req.user.hospital_id); + res.status(200).json(result); + } catch (error) { + console.error('Error fetching users:', error.message); + if (error.message.includes('Invalid hospital ID')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.getProfilePhoto = async (req, res) => { + try { + const userId = req.params.id; + const result = await userService.getProfilePhoto(userId, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error('Error fetching profile photo:', error.message); + if (error.message.includes('not authorized')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.login = async (req, res) => { + try { + const { email, password } = req.body; + const result = await userService.login(email, password); + res.status(200).json(result); + } catch (error) { + console.error('Login error:', error.message); + if (error.message.includes('Invalid email or password')) { + return res.status(401).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.logout = async (req, res) => { + try { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + const result = await userService.logout(token); + res.status(200).json(result); + } catch (error) { + console.error('Error during logout:', error.message); + if (error.message.includes('Access token required')) { + return res.status(401).json({ error: error.message }); + } + if (error.message.includes('Unauthorized access')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, 'uploads/profile_photos'); + }, + filename: (req, file, cb) => { + const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}${path.extname(file.originalname)}`; + cb(null, `${file.fieldname}-${uniqueSuffix}`); + }, +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed'), false); + } + }, + limits: { fileSize: 5 * 1024 * 1024 }, // Limit file size to 5 MB +}).single('profile_photo'); + +exports.uploadProfilePhoto = async (req, res) => { + upload(req, res, async (err) => { + if (err) { + console.error('Error uploading file:', err.message); + return res.status(400).json({ error: err.message }); + } + + try { + const result = await userService.uploadProfilePhoto(req.user.id, req.file); + res.status(200).json(result); + } catch (error) { + console.error('Error updating photo URL in database:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } + }); +}; + +exports.editHospitalUser = async (req, res) => { + try { + const { id } = req.params; + const result = await userService.editHospitalUser(id, req.body, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error('Error editing hospital user:', error.message); + if (error.message.includes('Access denied')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + if (error.message.includes('No valid fields')) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.deleteHospitalUser = async (req, res) => { + try { + const { id } = req.params; + const result = await userService.deleteHospitalUser(id, req.user.role); + res.status(200).json(result); + } catch (error) { + console.error('Error deleting hospital user:', error.message); + if (error.message.includes('Access denied')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.getAccessToken = async (req, res) => { + try { + const { refreshToken, user_id } = req.body; + const result = await userService.getAccessToken(refreshToken, user_id); + res.status(200).json(result); + } catch (error) { + console.error('Error generating access token:', error.message); + if (error.message.includes('required')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Invalid or expired')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.getAccessTokenForSpurrinadmin = async (req, res) => { + try { + const { refreshToken, user_id } = req.body; + const result = await userService.getAccessTokenForSpurrinadmin(refreshToken, user_id); + res.status(200).json(result); + } catch (error) { + console.error('Error generating access token:', error.message); + if (error.message.includes('required')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Invalid or expired')) { + return res.status(403).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.getRefreshTokenByUserId = async (req, res) => { + try { + const { user_id, role_id } = req.params; + const result = await userService.getRefreshTokenByUserId(user_id, role_id); + res.status(200).json(result); + } catch (error) { + console.error('Error fetching refresh token:', error.message); + if (error.message.includes('Invalid role_id')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.getHospitalUserId = async (req, res) => { + try { + const { email, password } = req.body; + const result = await userService.getHospitalUserId(email, password); + res.status(200).json(result); + } catch (error) { + console.error('Error fetching hospital user:', error.message); + if (error.message.includes('required')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Invalid email or password') || error.message.includes('not found')) { + return res.status(401).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +exports.updatePassword = async (req, res) => { + try { + const { id } = req.params; + const { new_password } = req.body; + const authHeader = req.headers.authorization; + const token = authHeader && authHeader.split(' ')[1]; + const result = await userService.updatePassword(id, new_password, token); + res.status(200).json(result); + } catch (error) { + console.error('Error updating password:', error.message); + if (error.message.includes('required')) { + return res.status(400).json({ error: error.message }); + } + if (error.message.includes('Invalid or expired token')) { + return res.status(401).json({ error: error.message }); + } + if (error.message.includes('Token user does not match')) { + return res.status(403).json({ error: error.message }); + } + if (error.message.includes('not found')) { + return res.status(404).json({ error: error.message }); + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +module.exports \ No newline at end of file diff --git a/src/middlewares/authMiddleware.js b/src/middlewares/authMiddleware.js new file mode 100644 index 0000000..c031e29 --- /dev/null +++ b/src/middlewares/authMiddleware.js @@ -0,0 +1,187 @@ +const jwt = require('jsonwebtoken'); +const db = require('../config/database'); + +exports.authenticateSuperAdmin = async (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Access token required' }); + } + + try { + // Verify the token + const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); + + // Ensure `id` exists in the token + if (!decoded.id) { + return res.status(403).json({ error: 'Invalid token: Missing SuperAdmin ID' }); + } + + const superAdminId = decoded.id; + + // Fetch SuperAdmin from DB + const query = `SELECT id, role_id, access_token FROM super_admins WHERE id = ?`; + const result = await db.query(query, [superAdminId]); + + if (!result || result.length === 0) { + return res.status(403).json({ error: 'Unauthorized: SuperAdmin user not found' }); + } + + const user = result[0]; + + // Ensure the role_id is 6 (Spurrinadmin) + if (user.role_id !== 6) { + return res.status(403).json({ error: 'Unauthorized: Not a SuperAdmin (Spurrinadmin)' }); + } + + // Ensure the token matches the stored token + if (user.access_token !== token) { + return res.status(403).json({ error: 'Invalid or mismatched access token', logout: true }); + } + + req.user = { id: user.id, role: 'Spurrinadmin' }; + next(); + } catch (error) { + return res.status(403).json({ + error: error.name === 'TokenExpiredError' ? 'Access token has expired' : 'Invalid access token', + logout: true + }); + } +}; + + +exports.authenticateToken = async (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Access token required' }); + } + + try { + // Verify the token + const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); + + // Ensure `id` exists in the token + if (!decoded.id) { + return res.status(403).json({ error: 'Invalid token: Missing User ID', logout: true }); + } + + const userId = decoded.id; + + // Determine the correct table and query based on the role + let table; + let query; + if (decoded.role === 'Spurrinadmin') { + table = 'super_admins'; + query = `SELECT access_token, access_token_expiry FROM ${table} WHERE id = ?`; + } else if (['Admin', 'Viewer', 'Superadmin',8,9,7].includes(decoded.role)) { + table = 'hospital_users'; + query = `SELECT access_token, access_token_expiry, hospital_id,hospital_code FROM ${table} WHERE id = ?`; + } else if (decoded.role === 'AppUser') { + table = 'app_users'; + query = `SELECT access_token, access_token_expiry,hospital_code FROM ${table} WHERE id = ?`; + } else { + return res.status(403).json({ error: 'Invalid role' }); + } + + + // Execute the query and validate the result + const result = await db.query(query, [userId]); + if (!result || result.length === 0) { + return res.status(403).json({ error: 'Unauthorized access: User not found' }); + } + + const user = result[0]; + + // Ensure the token matches the stored token + if (user.access_token !== token) { + return res.status(403).json({ error: 'Invalid or mismatched access token', logout: true }); + } + + req.user = { + id: userId, + role: decoded.role, + hospital_id: table === 'hospital_users' ? user.hospital_id || 0 : null, // Default to 0 if hospital_id is null + hospital_code: user.hospital_code || null, + }; + + next(); + } catch (error) { + return res.status(403).json({ error: 'Invalid or expired access token', logout: true }); + } +}; + + + +exports.authenticateOverHospitalStatus = async (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Access token required' }); + } + + try { + // Verify the token + const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); + + // Ensure `id` exists in the token + if (!decoded.id) { + return res.status(403).json({ error: 'Invalid token: Missing User ID', logout: true }); + } + + const userId = decoded.id; + + // Determine the correct table and query based on the role + let table; + let query; + + if (decoded.role === 'Spurrinadmin') { + table = 'super_admins'; + query = `SELECT access_token, access_token_expiry FROM ${table} WHERE id = ?`; + next(); + } else if (['Admin', 'Viewer', 'Superadmin',8,9,7].includes(decoded.role)) { + table = 'hospital_users'; + query = `SELECT access_token, access_token_expiry, hospital_id,hospital_code FROM ${table} WHERE id = ?`; + const result = await db.query(query, [userId]); + hsptquery = `SELECT status FROM hospitals WHERE id = ?`; + const hsptresult = await db.query(hsptquery, [result[0].hospital_id]); + if (hsptresult[0].status==='Inactive') { + return res.status(403).json({ error: 'Unauthorized access: Hospital is Inactive' }); + } + next(); + + } else if (decoded.role === 'AppUser') { + table = 'app_users'; + query = `SELECT access_token, access_token_expiry,hospital_code FROM ${table} WHERE id = ?`; + next(); + } else { + return res.status(403).json({ error: 'Invalid role' }); + } + // next(); + } catch (error) { + return res.status(403).json({ error: 'Invalid or expired access token', logout: true }); + } +}; + + +exports.authorizeRoles = (allowedRoles) => { + return (req, res, next) => { + const { role } = req.user; // Assuming role is stored in `req.user` by authMiddleware + + if (!allowedRoles.includes(role)) { + return res.status(403).json({ error: 'Access denied' }); + } + + next(); + }; +}; + +/********************************************************************* + * Company: Tech4biz Solutions + * Author: Tech4biz Solutions team backend + * Description: Authenticates user based on roles + * Copyright: Copyright © 2025Tech4Biz Solutions. + *********************************************************************/ \ No newline at end of file diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js new file mode 100644 index 0000000..edb713a --- /dev/null +++ b/src/middlewares/errorHandler.js @@ -0,0 +1,73 @@ +const { AppError } = require('../utils/errors'); +const logger = require('../utils/logger'); + +/** + * Global error handling middleware + */ +const errorHandler = (err, req, res, next) => { + err.statusCode = err.statusCode || 500; + err.status = err.status || 'error'; + + // Log error + logger.error({ + message: err.message, + stack: err.stack, + path: req.path, + method: req.method, + ip: req.ip, + user: req.user?.id + }); + + // Development error response + if (process.env.NODE_ENV === 'development') { + res.status(err.statusCode).json({ + status: err.status, + error: err, + message: err.message, + stack: err.stack + }); + } + // Production error response + else { + // Operational, trusted error: send message to client + if (err.isOperational) { + res.status(err.statusCode).json({ + status: err.status, + message: err.message + }); + } + // Programming or other unknown error: don't leak error details + else { + logger.error('UNEXPECTED ERROR 💥', err); + res.status(500).json({ + status: 'error', + message: 'Something went wrong' + }); + } + } +}; + +// Handle specific error types +const handleSequelizeError = (err) => { + if (err.name === 'SequelizeValidationError') { + return new AppError(err.message, 400); + } + if (err.name === 'SequelizeUniqueConstraintError') { + return new AppError('Duplicate field value entered', 400); + } + return err; +}; + +const handleJWTError = () => + new AppError('Invalid token. Please log in again!', 401); + +const handleJWTExpiredError = () => + new AppError('Your token has expired! Please log in again.', 401); + +module.exports = { + AppError, + errorHandler, + handleSequelizeError, + handleJWTError, + handleJWTExpiredError +}; \ No newline at end of file diff --git a/src/middlewares/security.js b/src/middlewares/security.js new file mode 100644 index 0000000..25b8d99 --- /dev/null +++ b/src/middlewares/security.js @@ -0,0 +1,114 @@ +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +const config = require('../config'); +const logger = require('../utils/logger'); + +// Rate limiting configuration +const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10000000, // High limit for now; tune for production + message: 'Too many requests from this IP, please try again later.', + handler: (req, res) => { + logger.warn(`Rate limit exceeded: ${req.ip} at ${new Date().toISOString()}`); + res.status(429).json({ + status: 'error', + message: 'Too many requests from this IP, please try again later.' + }); + } +}); + +// Security headers configuration +const securityHeaders = helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "wss:", "https:"], + fontSrc: ["'self'", "https:", "data:"], + objectSrc: ["'none'"], + mediaSrc: ["'self'"], + frameSrc: ["*"], + frameAncestors: ["*"] + } + }, + crossOriginEmbedderPolicy: true, + crossOriginOpenerPolicy: true, + + // ✅ FIX: Allow cross-origin loading of resources like images + crossOriginResourcePolicy: { policy: "cross-origin" }, + + dnsPrefetchControl: { allow: false }, + frameguard: { action: "deny" }, + hidePoweredBy: true, + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true + }, + ieNoOpen: true, + noSniff: true, + referrerPolicy: { policy: "strict-origin-when-cross-origin" }, + xssFilter: true +}); + +// Request validation middleware +const validateRequest = (req, res, next) => { + if (['POST', 'PUT'].includes(req.method) && !req.is('application/json') && !req.is('multipart/form-data')) { + return res.status(415).json({ + status: 'error', + message: 'Unsupported Media Type. Only application/json or multipart/form-data is allowed for POST/PUT requests.' + }); + } + next(); +}; + +// CORS configuration +const corsOptions = { + origin: (origin, callback) => { + if (!origin) return callback(null, true); + + const allowedOrigins = [ + 'http://192.168.1.19:8081', + 'http://localhost:5173', + 'http://localhost:5174', + 'https://spurrinai.com', + 'https://www.spurrinai.com', + 'http://localhost:3000', + 'https://www.spurrinai.org', + 'https://www.spurrinai.info', + 'https://spurrinai.info', + 'http://spurrinai.info', + 'https://34a4-122-171-20-117.ngrok-free.app', + 'http://34a4-122-171-20-117.ngrok-free.app' + ]; + + const isOriginAllowed = ( + /^http:\/\/[a-z0-9-]+\.localhost(:\d+)?$/.test(origin) || + /^https:\/\/[a-z0-9-]+\.spurrinai\.com$/.test(origin) || + /^https:\/\/[a-z0-9-]+\.spurrinai\.org$/.test(origin) || + /^https:\/\/[a-z0-9-]+\.spurrinai\.info$/.test(origin) || + allowedOrigins.includes(origin) + ); + + if (isOriginAllowed) { + callback(null, true); + } else { + logger.warn(`CORS blocked request from origin: ${origin}`); + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], + exposedHeaders: ['Content-Range', 'X-Content-Range'], + maxAge: 86400 +}; + +module.exports = { + apiLimiter, + securityHeaders, + validateRequest, + corsOptions +}; \ No newline at end of file diff --git a/src/middlewares/uploadsMiddleware.js b/src/middlewares/uploadsMiddleware.js new file mode 100644 index 0000000..688607d --- /dev/null +++ b/src/middlewares/uploadsMiddleware.js @@ -0,0 +1,52 @@ +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); + +// select folder to upload the image +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = "uploads/profile_photos/"; + + if (!fs.existsSync(uploadPath)) { + fs.mkdirSync(uploadPath, { recursive: true }); + } + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + console.log('Multer filename function - file.originalname:', file.originalname); + const fileExtension = path.extname(file.originalname); + console.log('Multer filename function - fileExtension:', fileExtension); + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + cb(null, `${file.fieldname}-${uniqueSuffix}${fileExtension}`); + }, +}); + +// choose only image +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed'), false); + } + }, + limits: { fileSize: 500 * 1024 * 1024 }, +}); + +const profilePhotoUploadMiddleware = (req, res, next) => { + upload.single('profile_photo')(req, res, function (err) { + if (err instanceof multer.MulterError) { + // A Multer error occurred when uploading. + return res.status(400).json({ error: err.message }); + } else if (err) { + // An unknown error occurred when uploading. + return res.status(500).json({ error: err.message }); + } + + // Everything went fine, proceed to the next middleware or controller + next(); + }); +}; + +module.exports = profilePhotoUploadMiddleware; \ No newline at end of file diff --git a/src/middlewares/validateRequest.js b/src/middlewares/validateRequest.js new file mode 100644 index 0000000..733a568 --- /dev/null +++ b/src/middlewares/validateRequest.js @@ -0,0 +1,34 @@ +const { ValidationError } = require('../utils/errors'); +const logger = require('../utils/logger'); + +/** + * Middleware to validate request data against a Joi schema + * @param {Object} schema - Joi validation schema + * @returns {Function} Express middleware function + */ +const validateRequest = (schema) => { + return (req, res, next) => { + try { + const { error } = schema.validate(req.body, { + abortEarly: false, + stripUnknown: true, + allowUnknown: false + }); + + if (error) { + const errorMessage = error.details + .map(detail => detail.message) + .join(', '); + + logger.warn(`Validation error: ${errorMessage}`); + throw new ValidationError(errorMessage); + } + + next(); + } catch (error) { + next(error); + } + }; +}; + +module.exports = validateRequest; \ No newline at end of file diff --git a/src/migrations/createMigration.js b/src/migrations/createMigration.js new file mode 100644 index 0000000..05e1835 --- /dev/null +++ b/src/migrations/createMigration.js @@ -0,0 +1,46 @@ +const fs = require('fs').promises; +const path = require('path'); +async function createMigration() { + try { + const name = process.argv[2]; + if (!name) { + console.error('Please provide a migration name'); + console.log('Usage: node createMigration.js '); + process.exit(1); + } + + const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0]; + const fileName = `${timestamp}_${name}.js`; + const filePath = path.join(__dirname, 'migrations', fileName); + + const template = `const db = require('../../config/database'); + + module.exports = { + async up() { + // Add your migration SQL here + // Example: + // await db.query(\` + // CREATE TABLE IF NOT EXISTS table_name ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // name VARCHAR(255) NOT NULL, + // created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + // ) + // \`); + }, + + async down() { + // Add your rollback SQL here + // Example: + // await db.query('DROP TABLE IF EXISTS table_name'); + } + };`; + + await fs.writeFile(filePath, template); + console.log(`Created migration file: ${fileName}`); + } catch (error) { + console.error('Error creating migration:', error); + process.exit(1); + } +} + +createMigration(); \ No newline at end of file diff --git a/src/migrations/migrationRunner.js b/src/migrations/migrationRunner.js new file mode 100644 index 0000000..e770a82 --- /dev/null +++ b/src/migrations/migrationRunner.js @@ -0,0 +1,157 @@ +const db = require('../config/database'); +const fs = require('fs').promises; +const path = require('path'); + +class MigrationRunner { + constructor() { + this.migrationsTable = 'migrations'; + } + + async initialize() { + try { + // Create migrations table if it doesn't exist + await db.query(` + CREATE TABLE IF NOT EXISTS ${this.migrationsTable} ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + } catch (error) { + console.error('Error initializing migrations table:', error); + throw error; + } + } + + async getExecutedMigrations() { + try { + const rows = await db.query( + `SELECT name FROM ${this.migrationsTable} ORDER BY executed_at ASC` + ); + // MySQL2 returns [rows, fields] where rows is an array + return Array.isArray(rows) ? rows.map(row => row.name) : []; + } catch (error) { + // If table doesn't exist, return empty array + if (error.code === 'ER_NO_SUCH_TABLE') { + return []; + } + throw error; + } + } + + async runMigrations() { + try { + await this.initialize(); + const executedMigrations = await this.getExecutedMigrations(); + + const migrationsDir = path.join(__dirname, 'migrations'); + const files = await fs.readdir(migrationsDir); + const migrationFiles = files + .filter(f => f.endsWith('.js')) + .sort(); + + for (const file of migrationFiles) { + if (!executedMigrations.includes(file)) { + console.log(`Running migration: ${file}`); + const migration = require(path.join(migrationsDir, file)); + + // Start transaction + await db.query('START TRANSACTION'); + + try { + await migration.up(); + // Use INSERT IGNORE to handle duplicate entries gracefully + await db.query( + `INSERT IGNORE INTO ${this.migrationsTable} (name) VALUES (?)`, + [file] + ); + await db.query('COMMIT'); + console.log(`Successfully executed migration: ${file}`); + } catch (error) { + await db.query('ROLLBACK'); + // If it's a duplicate entry error, just log and continue + if (error.code === 'ER_DUP_ENTRY') { + console.log(`Migration ${file} was already executed, skipping...`); + continue; + } + console.error(`Error executing migration ${file}:`, error); + throw error; + } + } else { + console.log(`Migration ${file} was already executed, skipping...`); + } + } + + console.log('All migrations completed successfully'); + } catch (error) { + console.error('Migration failed:', error); + throw error; + } + } + + async rollback() { + try { + // First check if migrations table exists and has entries + const migrations = await db.query(` + SELECT COUNT(*) as count + FROM ${this.migrationsTable} + `).catch(() => [{ count: 0 }]); + + + if (migrations[0].count === 0) { + // If no migrations in table, check if any migrations exist in directory + const migrationsDir = path.join(__dirname, 'migrations'); + const files = await fs.readdir(migrationsDir); + const migrationFiles = files + .filter(f => f.endsWith('.js')) + .sort(); + + if (migrationFiles.length === 0) { + console.log('No migrations found to rollback'); + return; + } + + // If migrations exist but aren't tracked, ask user what to do + console.log('Found migrations in directory but none are tracked in the database.'); + console.log('Available migrations:'); + migrationFiles.forEach(file => console.log(`- ${file}`)); + console.log('\nTo rollback a specific migration, please run:'); + console.log('npm run migrate:down -- --migration='); + return; + } + + const executedMigrations = await this.getExecutedMigrations(); + if (executedMigrations.length === 0) { + console.log('No migrations to rollback'); + return; + } + + const lastMigration = executedMigrations[executedMigrations.length - 1]; + console.log(`Rolling back migration: ${lastMigration}`); + + const migration = require(path.join(__dirname, 'migrations', lastMigration)); + + // Start transaction + await db.query('START TRANSACTION'); + + try { + await migration.down(); + await db.query( + `DELETE FROM ${this.migrationsTable} WHERE name = ?`, + [lastMigration] + ); + await db.query('COMMIT'); + console.log(`Successfully rolled back migration: ${lastMigration}`); + } catch (error) { + await db.query('ROLLBACK'); + console.error(`Error rolling back migration ${lastMigration}:`, error); + throw error; + } + } catch (error) { + console.error('Rollback failed:', error); + throw error; + } + } +} + +module.exports = new MigrationRunner(); \ No newline at end of file diff --git a/src/migrations/migrations/20240315000016_add_is_liked_to_interaction_logs.js b/src/migrations/migrations/20240315000016_add_is_liked_to_interaction_logs.js new file mode 100644 index 0000000..bd58840 --- /dev/null +++ b/src/migrations/migrations/20240315000016_add_is_liked_to_interaction_logs.js @@ -0,0 +1,45 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if column exists + const columns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'interaction_logs' + AND COLUMN_NAME = 'is_liked' + `); + + + // Only add column if it doesn't exist + if (columns.length === 0) { + await db.query(` + ALTER TABLE interaction_logs + ADD COLUMN is_liked BOOLEAN DEFAULT FALSE AFTER response + `); + console.log('Added is_liked column to interaction_logs table'); + } else { + console.log('is_liked column already exists in interaction_logs table'); + } + }, + + async down() { + // Check if column exists before dropping + const columns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'interaction_logs' + AND COLUMN_NAME = 'is_liked' + `); + + if (columns.length > 0) { + await db.query(` + ALTER TABLE interaction_logs + DROP COLUMN is_liked + `); + console.log('Dropped is_liked column from interaction_logs table'); + } else { + console.log('is_liked column does not exist in interaction_logs table'); + } + } +}; \ No newline at end of file diff --git a/src/migrations/migrations/20240315000017_add_public_signup_enabled_to_hospitals_app_users.js b/src/migrations/migrations/20240315000017_add_public_signup_enabled_to_hospitals_app_users.js new file mode 100644 index 0000000..0d47063 --- /dev/null +++ b/src/migrations/migrations/20240315000017_add_public_signup_enabled_to_hospitals_app_users.js @@ -0,0 +1,44 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if column exists + const columns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospitals' + AND COLUMN_NAME = 'publicSignupEnabled' + `); + + // Only add column if it doesn't exist + if (columns.length === 0) { + await db.query(` + ALTER TABLE hospitals + ADD COLUMN publicSignupEnabled BOOLEAN DEFAULT FALSE AFTER status + `); + console.log('Added publicSignupEnabled column to hospitals table'); + } else { + console.log('publicSignupEnabled column already exists in hospitals table'); + } + }, + + async down() { + // Check if column exists before dropping + const columns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospitals' + AND COLUMN_NAME = 'publicSignupEnabled' + `); + + if (columns.length > 0) { + await db.query(` + ALTER TABLE hospitals + DROP COLUMN publicSignupEnabled + `); + console.log('Dropped publicSignupEnabled column from hospitals table'); + } else { + console.log('publicSignupEnabled column does not exist in hospitals table'); + } + } +}; \ No newline at end of file diff --git a/src/migrations/migrations/20240723000000_add_temporary_password_to_hospitals_and_hospital_users.js b/src/migrations/migrations/20240723000000_add_temporary_password_to_hospitals_and_hospital_users.js new file mode 100644 index 0000000..288e1f1 --- /dev/null +++ b/src/migrations/migrations/20240723000000_add_temporary_password_to_hospitals_and_hospital_users.js @@ -0,0 +1,79 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Add temporary_password to hospitals table + const hospitalsColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospitals' + AND COLUMN_NAME = 'temporary_password' + `); + + if (hospitalsColumns.length === 0) { + await db.query(` + ALTER TABLE hospitals + ADD COLUMN temporary_password VARCHAR(255) NULL + `); + console.log('Added temporary_password column to hospitals table'); + } else { + console.log('temporary_password column already exists in hospitals table'); + } + + // Add temporary_password to hospital_users table + const hospitalUsersColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospital_users' + AND COLUMN_NAME = 'temporary_password' + `); + + if (hospitalUsersColumns.length === 0) { + await db.query(` + ALTER TABLE hospital_users + ADD COLUMN temporary_password VARCHAR(255) NULL + `); + console.log('Added temporary_password column to hospital_users table'); + } else { + console.log('temporary_password column already exists in hospital_users table'); + } + }, + + async down() { + // Drop temporary_password from hospitals table + const hospitalsColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospitals' + AND COLUMN_NAME = 'temporary_password' + `); + + if (hospitalsColumns.length > 0) { + await db.query(` + ALTER TABLE hospitals + DROP COLUMN temporary_password + `); + console.log('Dropped temporary_password column from hospitals table'); + } else { + console.log('temporary_password column does not exist in hospitals table'); + } + + // Drop temporary_password from hospital_users table + const hospitalUsersColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospital_users' + AND COLUMN_NAME = 'temporary_password' + `); + + if (hospitalUsersColumns.length > 0) { + await db.query(` + ALTER TABLE hospital_users + DROP COLUMN temporary_password + `); + console.log('Dropped temporary_password column from hospital_users table'); + } else { + console.log('temporary_password column does not exist in hospital_users table'); + } + } +}; \ No newline at end of file diff --git a/src/migrations/migrations/20240726120000_modify_feedback_table_columns.js b/src/migrations/migrations/20240726120000_modify_feedback_table_columns.js new file mode 100644 index 0000000..e5b6f47 --- /dev/null +++ b/src/migrations/migrations/20240726120000_modify_feedback_table_columns.js @@ -0,0 +1,77 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if column exists and is NOT NULL before altering + const ratingColumn = await db.query(` + SELECT IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'feedback' + AND COLUMN_NAME = 'rating' + `); + + const informationReceivedColumn = await db.query(` + SELECT IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'feedback' + AND COLUMN_NAME = 'information_received' + `); + + if (ratingColumn.length > 0 && ratingColumn[0].IS_NULLABLE === 'NO') { + await db.query(` + ALTER TABLE feedback + MODIFY COLUMN rating ENUM('Terrible', 'Bad', 'Okay', 'Good', 'Awesome') NULL + `); + console.log('Modified rating column to be nullable in feedback table'); + } else { + console.log('rating column is already nullable or does not exist in feedback table'); + } + + if (informationReceivedColumn.length > 0 && informationReceivedColumn[0].IS_NULLABLE === 'NO') { + await db.query(` + ALTER TABLE feedback + MODIFY COLUMN information_received ENUM('Yes', 'Partially', 'No') NULL + `); + console.log('Modified information_received column to be nullable in feedback table'); + } else { + console.log('information_received column is already nullable or does not exist in feedback table'); + } + }, + + async down() { + // Revert columns to NOT NULL if they are currently nullable + const ratingColumn = await db.query(` + SELECT IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'feedback' + AND COLUMN_NAME = 'rating' + `); + + const informationReceivedColumn = await db.query(` + SELECT IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'feedback' + AND COLUMN_NAME = 'information_received' + `); + + if (ratingColumn.length > 0 && ratingColumn[0].IS_NULLABLE === 'YES') { + await db.query(` + ALTER TABLE feedback + MODIFY COLUMN rating ENUM('Terrible', 'Bad', 'Okay', 'Good', 'Awesome') NOT NULL + `); + console.log('Reverted rating column to NOT NULL in feedback table'); + } else { + console.log('rating column is already NOT NULL or does not exist in feedback table'); + } + + if (informationReceivedColumn.length > 0 && informationReceivedColumn[0].IS_NULLABLE === 'YES') { + await db.query(` + ALTER TABLE feedback + MODIFY COLUMN information_received ENUM('Yes', 'Partially', 'No') NOT NULL + `); + console.log('Reverted information_received column to NOT NULL in feedback table'); + } else { + console.log('information_received column is already NOT NULL or does not exist in feedback table'); + } + } +}; \ No newline at end of file diff --git a/src/migrations/migrations/app_users_pin_otp_setup.js b/src/migrations/migrations/app_users_pin_otp_setup.js new file mode 100644 index 0000000..afd2772 --- /dev/null +++ b/src/migrations/migrations/app_users_pin_otp_setup.js @@ -0,0 +1,67 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if columns already exist to avoid duplicate ALTERs + const existingColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'app_users' + AND COLUMN_NAME IN ('pin_otp', 'pin_otp_expiry') + `); + + const existingColumnNames = existingColumns.map(col => col.COLUMN_NAME); + + if (!existingColumnNames.includes('pin_otp')) { + await db.query(` + ALTER TABLE app_users + ADD COLUMN pin_otp VARCHAR(6) + `); + console.log('Added pin_otp column to app_users table'); + } else { + console.log('pin_otp column already exists in app_users table'); + } + + if (!existingColumnNames.includes('pin_otp_expiry')) { + await db.query(` + ALTER TABLE app_users + ADD COLUMN pin_otp_expiry DATETIME + `); + console.log('Added pin_otp_expiry column to app_users table'); + } else { + console.log('pin_otp_expiry column already exists in app_users table'); + } + }, + + async down() { + // Drop the columns only if they exist + const existingColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'app_users' + AND COLUMN_NAME IN ('pin_otp', 'pin_otp_expiry') + `); + + const existingColumnNames = existingColumns.map(col => col.COLUMN_NAME); + + if (existingColumnNames.includes('pin_otp')) { + await db.query(` + ALTER TABLE app_users + DROP COLUMN pin_otp + `); + console.log('Removed pin_otp column from app_users table'); + } else { + console.log('pin_otp column does not exist in app_users table'); + } + + if (existingColumnNames.includes('pin_otp_expiry')) { + await db.query(` + ALTER TABLE app_users + DROP COLUMN pin_otp_expiry + `); + console.log('Removed pin_otp_expiry column from app_users table'); + } else { + console.log('pin_otp_expiry column does not exist in app_users table'); + } + } +}; diff --git a/src/migrations/migrations/city_to_hospital_users.js b/src/migrations/migrations/city_to_hospital_users.js new file mode 100644 index 0000000..b9597cc --- /dev/null +++ b/src/migrations/migrations/city_to_hospital_users.js @@ -0,0 +1,44 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if the 'city' column already exists in the 'hospital_users' table + const existingColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospital_users' + AND COLUMN_NAME = 'city' + `); + + if (existingColumns.length === 0) { + // Add the 'city' column if it doesn't exist + await db.query(` + ALTER TABLE hospital_users + ADD COLUMN city VARCHAR(225) + `); + console.log('Added city column to hospital_users table'); + } else { + console.log('city column already exists in hospital_users table'); + } + }, + + async down() { + // Drop the 'city' column if it exists + const existingColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'hospital_users' + AND COLUMN_NAME = 'city' + `); + + if (existingColumns.length > 0) { + await db.query(` + ALTER TABLE hospital_users + DROP COLUMN city + `); + console.log('Removed city column from hospital_users table'); + } else { + console.log('city column does not exist in hospital_users table'); + } + } +}; diff --git a/src/migrations/migrations/delete_app_user_keep_data.js b/src/migrations/migrations/delete_app_user_keep_data.js new file mode 100644 index 0000000..a9d674c --- /dev/null +++ b/src/migrations/migrations/delete_app_user_keep_data.js @@ -0,0 +1,43 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Add deleted column to app_users table + const userColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'app_users' + AND COLUMN_NAME = 'deleted' + `); + + if (userColumns.length === 0) { + await db.query(` + ALTER TABLE app_users + ADD COLUMN deleted BOOLEAN DEFAULT FALSE + `); + console.log('✅ Added deleted column to app_users table'); + } else { + console.log('⚠️ deleted column already exists in app_users table'); + } + }, + + async down() { + // Drop deleted column from app_users table + const userColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'app_users' + AND COLUMN_NAME = 'deleted' + `); + + if (userColumns.length > 0) { + await db.query(` + ALTER TABLE app_users + DROP COLUMN deleted + `); + console.log('🗑️ Dropped deleted column from app_users table'); + } else { + console.log('⚠️ deleted column does not exist in app_users table'); + } + } +}; diff --git a/src/migrations/migrations/feedback_add_boolean_for_forwarded.js b/src/migrations/migrations/feedback_add_boolean_for_forwarded.js new file mode 100644 index 0000000..20be3fb --- /dev/null +++ b/src/migrations/migrations/feedback_add_boolean_for_forwarded.js @@ -0,0 +1,47 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if the column already exists + const existingColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'feedback' + AND COLUMN_NAME = 'is_forwarded' + `); + + const columnExists = existingColumns.length > 0; + + if (!columnExists) { + await db.query(` + ALTER TABLE feedback + ADD COLUMN is_forwarded BOOLEAN DEFAULT 0 + `); + console.log('✅ Added is_forwarded column to feedback table'); + } else { + console.log('⚠️ is_forwarded column already exists in feedback table'); + } + }, + + async down() { + // Check again before dropping + const existingColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'feedback' + AND COLUMN_NAME = 'is_forwarded' + `); + + const columnExists = existingColumns.length > 0; + + if (columnExists) { + await db.query(` + ALTER TABLE feedback + DROP COLUMN is_forwarded + `); + console.log('🗑️ Removed is_forwarded column from feedback table'); + } else { + console.log('⚠️ is_forwarded column does not exist in feedback table'); + } + } +}; diff --git a/src/migrations/migrations/sessions.js b/src/migrations/migrations/sessions.js new file mode 100644 index 0000000..8c15ea8 --- /dev/null +++ b/src/migrations/migrations/sessions.js @@ -0,0 +1,36 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if the 'sessions' table exists + const tables = await db.query(` + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'sessions' + `); + + // Only drop the table if it exists + if (tables.length > 0) { + await db.query('DROP TABLE sessions'); + console.log('Dropped sessions table'); + } else { + console.log('sessions table does not exist'); + } + }, + + async down() { + // Recreate the 'sessions' table if needed + await db.query(` + CREATE TABLE sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + session_token VARCHAR(255) NOT NULL, + user_agent VARCHAR(255), + ip_address VARCHAR(45), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `); + console.log('Recreated sessions table'); + } +}; diff --git a/src/migrations/migrations/super-admins-temporary-password.js b/src/migrations/migrations/super-admins-temporary-password.js new file mode 100644 index 0000000..791f198 --- /dev/null +++ b/src/migrations/migrations/super-admins-temporary-password.js @@ -0,0 +1,43 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Add temporary_password to super_admins table + const superAdminColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'super_admins' + AND COLUMN_NAME = 'temporary_password' + `); + + if (superAdminColumns.length === 0) { + await db.query(` + ALTER TABLE super_admins + ADD COLUMN temporary_password VARCHAR(255) NULL + `); + console.log('✅ Added temporary_password column to super_admins table'); + } else { + console.log('⚠️ temporary_password column already exists in super_admins table'); + } + }, + + async down() { + // Drop temporary_password from super_admins table + const superAdminColumns = await db.query(` + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'super_admins' + AND COLUMN_NAME = 'temporary_password' + `); + + if (superAdminColumns.length > 0) { + await db.query(` + ALTER TABLE super_admins + DROP COLUMN temporary_password + `); + console.log('🗑️ Dropped temporary_password column from super_admins table'); + } else { + console.log('⚠️ temporary_password column does not exist in super_admins table'); + } + } +}; \ No newline at end of file diff --git a/src/migrations/migrations/user_sessions_delete.js b/src/migrations/migrations/user_sessions_delete.js new file mode 100644 index 0000000..c7d6c6d --- /dev/null +++ b/src/migrations/migrations/user_sessions_delete.js @@ -0,0 +1,35 @@ +const db = require('../../config/database'); + +module.exports = { + async up() { + // Check if the 'user_sessions' table exists + const tables = await db.query(` + SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'user_sessions' + `); + + // Only drop the table if it exists + if (tables.length > 0) { + await db.query('DROP TABLE user_sessions'); + console.log('Dropped user_sessions table'); + } else { + console.log('user_sessions table does not exist'); + } + }, + + async down() { + // Recreate the 'user_sessions' table if needed + await db.query(` + CREATE TABLE user_sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + session_token VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `); + console.log('Recreated user_sessions table'); + } +}; diff --git a/src/migrations/runMigrations.js b/src/migrations/runMigrations.js new file mode 100644 index 0000000..a8cecd7 --- /dev/null +++ b/src/migrations/runMigrations.js @@ -0,0 +1,26 @@ +const migrationRunner = require('./migrationRunner'); + +async function run() { + try { + const command = process.argv[2]; + + switch (command) { + case 'up': + await migrationRunner.runMigrations(); + break; + case 'down': + await migrationRunner.rollback(); + break; + default: + console.log('Usage: node runMigrations.js [up|down]'); + process.exit(1); + } + + process.exit(0); + } catch (error) { + console.error('Migration error:', error); + process.exit(1); + } +} + +run(); \ No newline at end of file diff --git a/src/migrations/template.js b/src/migrations/template.js new file mode 100644 index 0000000..edf4f57 --- /dev/null +++ b/src/migrations/template.js @@ -0,0 +1,37 @@ +const db = require('../config/database'); + +/** + * Migration template + * + * To create a new migration: + * 1. Copy this file + * 2. Rename it with timestamp and description (e.g., 20240315000000_create_hospitals_table.js) + * 3. Implement up() and down() methods + * 4. Add your SQL queries + */ + +module.exports = { + /** + * Run the migration + */ + async up() { + // Add your migration SQL here + // Example: + // await db.query(` + // CREATE TABLE IF NOT EXISTS table_name ( + // id INT AUTO_INCREMENT PRIMARY KEY, + // name VARCHAR(255) NOT NULL, + // created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + // ) + // `); + }, + + /** + * Rollback the migration + */ + async down() { + // Add your rollback SQL here + // Example: + // await db.query('DROP TABLE IF EXISTS table_name'); + } +}; \ No newline at end of file diff --git a/src/routes/analysis.js b/src/routes/analysis.js new file mode 100644 index 0000000..f60f5bf --- /dev/null +++ b/src/routes/analysis.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const analysisController = require('../controllers/analysisController'); +const { authenticateToken } = require('../middlewares/authMiddleware'); +const multer = require('multer'); +const upload = multer(); +// Analysis routes +router.get('/hospitals/onboarded',upload.none(), authenticateToken, analysisController.getOnboardedHospitalsAnalysis); +router.post('/hospitals/active',upload.none(), authenticateToken, analysisController.getActiveHospitalsAnalysis); +router.get('/users/active',upload.none(), authenticateToken, analysisController.getActiveChatUsersAnalysis); +router.post('/hospitals/registered-users', upload.none(), authenticateToken, analysisController.getHospitalRegisteredUsers); +router.post('/hospitals/active-app-users', upload.none(), authenticateToken, analysisController.getHospitalActiveUsers); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/appUsers.js b/src/routes/appUsers.js new file mode 100644 index 0000000..360302a --- /dev/null +++ b/src/routes/appUsers.js @@ -0,0 +1,163 @@ +const express = require("express"); +const router = express.Router(); +const appUserController = require("../controllers/appUserController"); +const authMiddleware = require("../middlewares/authMiddleware"); +const db = require("../config/database"); // Database connection +// Ensure the upload middleware is properly applied +const multer = require("multer"); +const fs = require("fs"); +const path = require("path"); + +// Multer Configuration (add this if missing) +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = "uploads/id_photos/"; + if (!fs.existsSync(uploadPath)) { + fs.mkdirSync(uploadPath, { recursive: true }); + } + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + const fileExtension = path.extname(file.originalname); // Get proper file extension + cb(null, `id_photo-${uniqueSuffix}${fileExtension}`); // Ensure proper extension + }, +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/")) { + cb(null, true); + } else { + cb(new Error("Only image files are allowed"), false); + } + }, +}); + +router.post( + "/upload-id/:id", + authMiddleware.authenticateToken, + (req, res, next) => + upload.single("id_photo_url")(req, res, async (err) => { + if (err instanceof multer.MulterError || err) { + console.error("Multer error:", err.message); + return res.status(400).json({ error: err.message }); + } + + if (!req.file) { + return res.status(400).json({ error: "No file uploaded" }); + } + + const userId = req.params.id; + const filePath = `/uploads/id_photos/${req.file.filename}`; // Correct file path + + try { + const result = await db.query( + "UPDATE app_users SET upload_status = ?, id_photo_url = ? WHERE id = ?", + ["1", filePath, userId] + ); + + next(); + } catch (error) { + console.error("Database update error:", error.message); + return res + .status(500) + .json({ error: "Failed to update upload status" }); + } + }), + appUserController.uploadIdPhoto +); + +router.post("/login", appUserController.login); + +router.put( + "/approve-id/:id", + authMiddleware.authenticateToken, + upload.none(), // Middleware to validate the token + appUserController.approveUserId // Controller to handle the approval logic +); + +router.get( + "/hospital-users", + authMiddleware.authenticateToken, // Middleware to validate the access token + appUserController.getAppUsers // Controller to fetch app users +); + +router.get( + "/hospital-users/:id", + authMiddleware.authenticateToken, // Middleware to validate the access token + appUserController.getAppUserByHospitalId // Controller to fetch app users +); + +router.post("/signup", upload.single("id_photo_url"), appUserController.signup); + +router.post( + "/logout", + authMiddleware.authenticateToken, + appUserController.logout +); + +router.get( + "/appuser_status", + authMiddleware.authenticateToken, + appUserController.getAppUsersByHospitalCode +); + +router.delete( + "/delete/:id", + authMiddleware.authenticateToken, + appUserController.deleteAppUser +); + +// query title routes +router.put( + "/q-title", + authMiddleware.authenticateToken, + appUserController.updateQueryTitle +); + +router.post( + "/q-title", + upload.none(), // Middleware to validate the token + authMiddleware.authenticateToken, + appUserController.getShortTitle +); + +router.delete( + "/q-title", + upload.none(), // Middleware to validate the token + authMiddleware.authenticateToken, + appUserController.deleteQueryTitle +); + +// change password +router.put("/change-password", upload.none(), appUserController.changePassword); +router.post("/send-otp", upload.none(), appUserController.sendOtp); + +router.put("/change-pin", upload.none(), appUserController.changePinByOtp); +router.post("/send-pin-otp", upload.none(), appUserController.sendPinOtp); + +// chat sessions +router.get('/chat-sessions', authMiddleware.authenticateToken, appUserController.getChatSessionsByAppUserID); +router.get('/chat/:session_id', authMiddleware.authenticateToken, appUserController.getChatForEachSession); + +// delete chat sessions and chats do not delete logs make them inactive +router.put('/delete-session',upload.none() ,authMiddleware.authenticateToken, appUserController.deleteChatSessions); +router.put('/delete-chat',upload.none(), authMiddleware.authenticateToken, appUserController.clearChatbasedOnSessions); + +router.post('/chat-logs-bytime', upload.none(),authMiddleware.authenticateToken, appUserController.getChatByTime); +// check email and hospital_code +router.post('/check-email-code', upload.none(), appUserController.checkEmailCode); +// get popular topics +router.get('/popular-topics',authMiddleware.authenticateToken, appUserController.getPopularTopics); + +// Pin management routes +router.put('/change-pin', upload.none(), authMiddleware.authenticateToken, appUserController.changePin); +router.post('/forgot-pin', upload.none(), appUserController.forgotPin); +router.post('/verify-pin', upload.none(), appUserController.checkPin); + +router.put('/update-settings', upload.none(), authMiddleware.authenticateToken, appUserController.updateSettings); +router.put('/like', upload.none(), authMiddleware.authenticateToken, appUserController.hitlike); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..9bcfbb9 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,16 @@ +const express = require('express'); +const authController = require('../controllers/authController'); + + +const authMiddleware = require('../middlewares/authMiddleware'); + +const router = express.Router(); + +// common api endpoint for login +router.post('/login',authMiddleware.authenticateToken,authMiddleware.authenticateOverHospitalStatus, authController.login); +router.post('/refresh', authController.refreshToken); +router.post('/logout', authController.logout); + +router.post('/check-token',authController.checkAccessToken) + +module.exports = router; \ No newline at end of file diff --git a/src/routes/documents.js b/src/routes/documents.js new file mode 100644 index 0000000..5d8fcf2 --- /dev/null +++ b/src/routes/documents.js @@ -0,0 +1,66 @@ + +const express = require('express'); +const multer = require('multer'); +const authMiddleware = require('../middlewares/authMiddleware'); +const documentController = require('../controllers/documentsController'); +const fs = require('fs'); +const path = require('path'); +const router = express.Router(); + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = "uploads/documents/"; + + if (!fs.existsSync(uploadPath)) { + fs.mkdirSync(uploadPath, { recursive: true }); + } + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + cb(null, `${file.fieldname}-${uniqueSuffix}-${file.originalname}`); + }, +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + if (file.mimetype === 'application/pdf' || file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only PDF and image files are allowed'), false); + } + }, +}); + +// Document Upload API +router.post( + '/upload', + authMiddleware.authenticateToken, // Middleware to validate the token + upload.single('file'), // Middleware to handle file upload + documentController.uploadDocument // Controller to process the request +); + +router.get( + '/hospital/:hospital_id', + authMiddleware.authenticateToken, + authMiddleware.authorizeRoles(['Superadmin', 'Admin', 'Viewer', 8, 9, 7]), + documentController.getDocumentsByHospital +); + +router.put( + '/update-status/:id', + authMiddleware.authenticateToken, + authMiddleware.authorizeRoles(['Superadmin', 'Admin', 8, 7]), + documentController.updateDocumentStatus +); + +router.delete( + '/delete/:id', + authMiddleware.authenticateToken, + authMiddleware.authorizeRoles(['Superadmin', 'Admin', 8, 7]), + documentController.deleteDocument +); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/exceldata.js b/src/routes/exceldata.js new file mode 100644 index 0000000..4e334ef --- /dev/null +++ b/src/routes/exceldata.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const excelController = require('../controllers/exceldataController'); +const authMiddleware = require('../middlewares/authMiddleware'); +const multer = require('multer'); +const upload = multer(); + +router.post('/',authMiddleware.authenticateToken,upload.none(), excelController.createExcelEntry); + + +// put and delete will be done from hospital_users route as excel data is nothing but adding hospital_users through excel + +module.exports = router; diff --git a/src/routes/feedbacks.js b/src/routes/feedbacks.js new file mode 100644 index 0000000..ac11b42 --- /dev/null +++ b/src/routes/feedbacks.js @@ -0,0 +1,25 @@ +const express = require('express'); +const router = express.Router(); +const feedbacksController = require('../controllers/feedbacksController'); +const { authenticateToken } = require('../middlewares/authMiddleware'); +const multer = require('multer'); +const upload = multer(); + +// App user routes - for submitting feedback to hospitals +// Accepts: rating, purpose, information_received, feedback_text, improvement +router.post('/app-user/submit', upload.none(), authenticateToken, feedbacksController.createAppUserFeedback); + +// Hospital routes - for submitting feedback to Spurrin and viewing received feedbacks +// Accepts: rating, purpose, information_received, feedback_text, improvement +router.post('/hospital/submit', upload.none(), authenticateToken, feedbacksController.createHospitalFeedback); + +router.get('/hospital/received', authenticateToken, feedbacksController.getHospitalFeedbacks); +router.post('/hospital/forward',upload.none(), authenticateToken, feedbacksController.forwardAppUserFeedbacks); + +// Admin routes - for viewing all feedbacks +router.get('/admin/all', authenticateToken, feedbacksController.getAllFeedbacks); +router.get('/get-forwarded-feedbacks',authenticateToken,feedbacksController.getForwardedFeedbacks) + +router.delete('/:id',authenticateToken,feedbacksController.deleteAppUserFeedback); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/hospitals.js b/src/routes/hospitals.js new file mode 100644 index 0000000..b21d1cf --- /dev/null +++ b/src/routes/hospitals.js @@ -0,0 +1,158 @@ +const express = require("express"); +const multer = require("multer"); +const fs = require("fs"); +const jwt = require("jsonwebtoken"); // Make sure jwt is required +const authMiddleware = require("../middlewares/authMiddleware"); +const router = express.Router(); +const hospitalController = require("../controllers/hospitalController"); +const db = require("../config/database"); // Database connection + +// Route for creating hospital +router.post( + "/create-hospital", + authMiddleware.authenticateToken, + hospitalController.createHospital +); + +// Multer configuration to handle logo uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = "uploads/logos/"; + if (!fs.existsSync(uploadPath)) { + fs.mkdirSync(uploadPath, { recursive: true }); + } + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + const fileExtension = file.originalname.split(".").pop(); // Get the file extension + cb(null, `${file.fieldname}-${uniqueSuffix}.${fileExtension}`); // Append the extension + }, +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/")) { + cb(null, true); + } else { + cb(new Error("Only image files are allowed"), false); + } + }, +}); + + + +// Route for getting a list of hospitals +router.get( + "/list", + authMiddleware.authenticateToken, // Middleware to validate access token + hospitalController.getHospitalList +); +// Route for getting a hospital from list of hospital +router.get( + "/list/:id", + authMiddleware.authenticateToken, // Middleware to validate access token + hospitalController.getHospitalById +); + +// Route to update a hospital +router.put( + "/update/:id", + authMiddleware.authenticateToken, + hospitalController.updateHospital +); + +// Route to delete a hospital +router.delete( + "/delete/:id", + authMiddleware.authenticateToken, + hospitalController.deleteHospital +); + +// get all users of hospital +router.get( + "/users", + authMiddleware.authenticateToken, + hospitalController.getAllHospitalUsers +); + +// get colors from hospital +router.get( + "/colors", + authMiddleware.authenticateToken, + hospitalController.getColorsFromHospital +); + +// send temporary password to superadmin +router.post( + "/send-temp-password", + upload.none(), + hospitalController.sendTempPassword +); + +// change password of super_admins +router.post( + "/change-password", + upload.none(), + hospitalController.changeTempPassword +); + +// send temporary password to admin or viewer +router.post( + "/send-temp-password-av", + upload.none(), + hospitalController.sendTemporaryPassword +); + +// change password of admin and viewer +router.post( + "/change-password-av", + upload.none(), + hospitalController.changeTempPasswordAdminsViewers +); + +// update admin name +router.post( + "/update-admin-name", + upload.none(), + authMiddleware.authenticateToken, + hospitalController.updateHospitalName +); + +// check newly registered app user's notification +router.post( + "/check-user-notification", + upload.none(), + authMiddleware.authenticateToken, + hospitalController.checkNewAppUser +); + +// update app user's notification status +router.put( + "/update-user-notification/:id", + authMiddleware.authenticateToken, + hospitalController.updateAppUserChecked +); + +// app users interaction logs based on hospital_code +router.post( + "/interaction-logs", + upload.none(), + authMiddleware.authenticateToken, + hospitalController.interactionLogs +); + +// allow or restrict public signup and login +router.put( + "/public-signup/:id", + authMiddleware.authenticateToken, + hospitalController.updatePublicSignup +); + +router.get("/public-signup/:id", + authMiddleware.authenticateToken, + hospitalController.getPublicSignup +) + +module.exports = router; \ No newline at end of file diff --git a/src/routes/onboarding.js b/src/routes/onboarding.js new file mode 100644 index 0000000..032168e --- /dev/null +++ b/src/routes/onboarding.js @@ -0,0 +1,16 @@ +const express = require('express'); +const authMiddleware = require('../middlewares/authMiddleware'); +const onboardingController = require('../controllers/onboardingController'); + +const router = express.Router(); + +// Route to fetch all onboarding steps for a user +router.get('/:userId', authMiddleware.authenticateToken, onboardingController.getOnboardingSteps); + +// Route to add a new onboarding step +router.post('/add', authMiddleware.authenticateToken, onboardingController.addOnboardingStep); + +// Route to update an onboarding step +router.put('/update/:user_id', authMiddleware.authenticateToken, onboardingController.updateOnboardingStep); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/roles.js b/src/routes/roles.js new file mode 100644 index 0000000..9f5c8d4 --- /dev/null +++ b/src/routes/roles.js @@ -0,0 +1,8 @@ +const express = require('express'); +const roleController = require('../controllers/roleController'); + +const router = express.Router(); + +router.get('/', roleController.getAllRoles); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/superAdmins.js b/src/routes/superAdmins.js new file mode 100644 index 0000000..5e803fc --- /dev/null +++ b/src/routes/superAdmins.js @@ -0,0 +1,61 @@ +const express = require('express'); +const superAdminController = require('../controllers/superAdminController'); +const authMiddleware = require('../middlewares/authMiddleware'); + +const router = express.Router(); + +const multer = require("multer"); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = "uploads/logos/"; + if (!fs.existsSync(uploadPath)) { + fs.mkdirSync(uploadPath, { recursive: true }); + } + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); + const fileExtension = file.originalname.split(".").pop(); // Get the file extension + cb(null, `${file.fieldname}-${uniqueSuffix}.${fileExtension}`); // Append the extension + }, +}); +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith("image/")) { + cb(null, true); + } else { + cb(new Error("Only image files are allowed"), false); + } + }, +}); + + +// Route to create Spurrin SuperAdmin without authentication +router.post('/initialize', superAdminController.initializeSuperAdmin); + +router.get('/',authMiddleware.authenticateToken ,superAdminController.getAllSuperAdmins); +router.post('/',authMiddleware.authenticateToken ,superAdminController.addSuperAdmin); +router.delete('/:id',authMiddleware.authenticateToken, superAdminController.deleteSuperAdmin); +router.get( + '/data-consumption-report', + authMiddleware.authenticateToken, + superAdminController.getOnboardedHospitals +); + +// change password +router.post( + "/send-temp-password", + upload.none(), + + superAdminController.sendTempPassword +); +router.post( + "/change-password", + upload.none(), + + superAdminController.changeTempPassword +); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/users.js b/src/routes/users.js new file mode 100644 index 0000000..6c5c8f0 --- /dev/null +++ b/src/routes/users.js @@ -0,0 +1,78 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const userController = require('../controllers/userController'); + +const authMiddleware = require('../middlewares/authMiddleware'); +const authController = require('../controllers/authController'); + +const router = express.Router(); +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = "uploads/profile_photos/"; + + if (!fs.existsSync(uploadPath)) { + fs.mkdirSync(uploadPath, { recursive: true }); + } + cb(null, uploadPath); + // cb(null, 'uploads/profile_photos'); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + cb(null, `${file.fieldname}-${uniqueSuffix}${path.extname(file.originalname)}`); + }, +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed'), false); + } + }, + limits: { fileSize: 500 * 1024 * 1024 }, +}); + +// Route to add new user to hospital +router.post('/add-user', + authMiddleware.authenticateToken, + userController.addUser); + +// Edit hospital user +router.put('/edit-user/:id',upload.none(), authMiddleware.authenticateToken, userController.editHospitalUser); +router.delete('/delete-user/:id', upload.none(), authMiddleware.authenticateToken, userController.deleteHospitalUser); +router.post('/upload-profile-photo', authMiddleware.authenticateToken, userController.uploadProfilePhoto); + +router.post('/get-access-token', userController.getAccessToken); + +router.post('/get-spu-access-token', userController.getAccessTokenForSpurrinadmin); + +router.get('/refresh-token/:user_id', userController.getRefreshTokenByUserId); + +router.post('/hospital-users/login', userController.getHospitalUserId); + +// Route to update hospital user password +router.put( + '/update-password/:id', + upload.none(), + authMiddleware.authenticateToken, // Middleware to validate access token + userController.updatePassword +); + +router.post('/login', userController.login); // Login endpoint +router.post('/logout', userController.logout); // Logout endpoint + +// Define the route +router.get('/:hospital_id', + authController.authenticateToken, + userController.getUsersByHospital); + +router.get('/profile_photo/:id', + authController.authenticateToken, + userController.getProfilePhoto); + +router.get('/refresh-token/:user_id/:role_id', userController.getRefreshTokenByUserId); + +module.exports = router; \ No newline at end of file diff --git a/src/schema b/src/schema new file mode 100644 index 0000000..2dc6994 --- /dev/null +++ b/src/schema @@ -0,0 +1,520 @@ +-- MySQL dump 10.13 Distrib 8.0.41, for Linux (x86_64) +-- +-- Host: localhost Database: medquery +-- ------------------------------------------------------ +-- Server version 8.0.41-0ubuntu0.24.04.1 +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */ +; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */ +; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */ +; +/*!50503 SET NAMES utf8mb4 */ +; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */ +; +/*!40103 SET TIME_ZONE='+00:00' */ +; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */ +; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */ +; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */ +; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */ +; +-- +-- Table structure for table `app_users` +-- + +DROP TABLE IF EXISTS `app_users`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `app_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `email` varchar(255) NOT NULL, + `hash_password` varchar(255) NOT NULL, + `pin_number` VARCHAR(4) DEFAULT NULL, + `pin_enabled` BOOLEAN DEFAULT FALSE, + `remember_me` BOOLEAN DEFAULT FALSE, + `username` text, + `upload_status` enum('0', '1') DEFAULT '0', + `status` enum('Pending', 'Active', 'Inactive') DEFAULT 'Pending', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `hospital_code` varchar(12) DEFAULT NULL, + `id_photo_url` text, + `query_title` TEXT NULL DEFAULT NULL, + `otp_code` VARCHAR(6) DEFAULT NULL, + `otp_expires_at` DATETIME DEFAULT NULL, + `access_token` text, + `access_token_expiry` datetime DEFAULT NULL, + `checked` BOOLEAN DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `fk_hospital_code` (`hospital_code`), + CONSTRAINT `fk_hospital_code` FOREIGN KEY (`hospital_code`) REFERENCES `hospitals` (`hospital_code`) +) ENGINE = InnoDB AUTO_INCREMENT = 13 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `app_users` +-- + +-- +-- Table structure for table `audit_logs` +-- + +DROP TABLE IF EXISTS `audit_logs`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `audit_logs` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int DEFAULT NULL, + `table_name` varchar(255) DEFAULT NULL, + `operation` enum('INSERT', 'UPDATE', 'DELETE') DEFAULT NULL, + `changes_log` json DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `audit_logs` +-- + +LOCK TABLES `audit_logs` WRITE; +/*!40000 ALTER TABLE `audit_logs` DISABLE KEYS */ +; +/*!40000 ALTER TABLE `audit_logs` ENABLE KEYS */ +; +UNLOCK TABLES; +-- +-- Table structure for table `document_metadata` +-- + +DROP TABLE IF EXISTS `document_metadata`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `document_metadata` ( + `id` int NOT NULL AUTO_INCREMENT, + `document_id` int DEFAULT NULL, + `key_name` varchar(100) DEFAULT NULL, + `value_name` text, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `document_id` (`document_id`), + CONSTRAINT `document_metadata_ibfk_1` FOREIGN KEY (`document_id`) REFERENCES `documents` (`id`) +) ENGINE = InnoDB AUTO_INCREMENT = 62 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `document_metadata` +-- + +LOCK TABLES `document_metadata` WRITE; +/*!40000 ALTER TABLE `document_metadata` DISABLE KEYS */ +; +/*!40000 ALTER TABLE `document_metadata` ENABLE KEYS */ +; +UNLOCK TABLES; +-- +-- Table structure for table `document_pages` +-- + +DROP TABLE IF EXISTS `document_pages`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `document_pages` ( + `id` int NOT NULL AUTO_INCREMENT, + `document_id` int NOT NULL, + `page_number` int NOT NULL, + `content` LONGTEXT, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `document_id` (`document_id`), + CONSTRAINT `document_pages_ibfk_1` FOREIGN KEY (`document_id`) REFERENCES `documents` (`id`) ON DELETE CASCADE +) ENGINE = InnoDB AUTO_INCREMENT = 12306 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `document_pages` +-- + +-- +-- Table structure for table `documents` +-- + +DROP TABLE IF EXISTS `documents`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `documents` ( + `id` int NOT NULL AUTO_INCREMENT, + `hospital_id` int DEFAULT NULL, + `uploaded_by` int DEFAULT NULL, + `file_name` varchar(255) NOT NULL, + `file_url` text NOT NULL, + `uploaded_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `processed_status` enum('Pending', 'Processed', 'Failed') DEFAULT 'Pending', + `failed_page` int DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `reason` text, + PRIMARY KEY (`id`), + KEY `hospital_id` (`hospital_id`), + KEY `uploaded_by` (`uploaded_by`), + CONSTRAINT `documents_ibfk_1` FOREIGN KEY (`hospital_id`) REFERENCES `hospitals` (`id`), + CONSTRAINT `documents_ibfk_2` FOREIGN KEY (`uploaded_by`) REFERENCES `hospital_users` (`id`) +) ENGINE = InnoDB AUTO_INCREMENT = 58 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `documents` +-- + +-- +-- Table structure for table `hospital_users` +-- + +DROP TABLE IF EXISTS `hospital_users`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `hospital_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `hospital_id` int DEFAULT NULL, + `email` varchar(255) NOT NULL, + `hash_password` varchar(255) NOT NULL, + `expires_at` DATETIME DEFAULT NULL, + `type` VARCHAR(50) DEFAULT NULL, + `role_id` int DEFAULT NULL, + `is_default_admin` tinyint(1) DEFAULT '1', + `requires_onboarding` tinyint(1) DEFAULT '1', + `password_reset_required` tinyint(1) DEFAULT '1', + `profile_photo_url` text, + `phone_number` varchar(15) DEFAULT NULL, + `bio` text, + `status` enum('Active', 'Inactive') DEFAULT 'Active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `refresh_token` text, + `name` varchar(255) DEFAULT NULL, + `department` varchar(255) DEFAULT NULL, + `location` varchar(255) DEFAULT NULL, + `mobile_number` varchar(15) DEFAULT NULL, + `access_token` varchar(500) DEFAULT NULL, + `access_token_expiry` datetime DEFAULT NULL, + `hospital_code` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `hospital_id` (`hospital_id`), + KEY `role_id` (`role_id`), + CONSTRAINT `hospital_users_ibfk_1` FOREIGN KEY (`hospital_id`) REFERENCES `hospitals` (`id`), + CONSTRAINT `hospital_users_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) +) ENGINE = InnoDB AUTO_INCREMENT = 61 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `hospital_users` +-- + +-- +-- Table structure for table `hospitals` +-- + +DROP TABLE IF EXISTS `hospitals`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `hospitals` ( + `id` int NOT NULL AUTO_INCREMENT, + `name_hospital` varchar(255) NOT NULL, + `subdomain` varchar(255) NOT NULL, + `primary_admin_email` varchar(255) NOT NULL, + `primary_admin_password` varchar(255) NOT NULL, + `expires_at` DATETIME DEFAULT NULL, + `type` VARCHAR(50) DEFAULT NULL, + `primary_color` varchar(20) DEFAULT NULL, + `secondary_color` varchar(20) DEFAULT NULL, + `logo_url` text, + `status` enum('Active', 'Inactive') DEFAULT 'Active', + `onboarding_status` enum('Pending', 'Completed') DEFAULT 'Pending', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `admin_name` varchar(255) NOT NULL, + `mobile_number` varchar(15) NOT NULL, + `location` varchar(255) NOT NULL, + `super_admin_id` int NOT NULL, + `hospital_code` varchar(12) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `subdomain` (`subdomain`), + UNIQUE KEY `hospital_code` (`hospital_code`), + UNIQUE KEY `hospital_code_2` (`hospital_code`), + KEY `fk_super_admin_id` (`super_admin_id`), + CONSTRAINT `fk_super_admin_id` FOREIGN KEY (`super_admin_id`) REFERENCES `super_admins` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE = InnoDB AUTO_INCREMENT = 54 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `hospitals` +-- + +-- +-- Table structure for table `interaction_logs` +-- + +DROP TABLE IF EXISTS `interaction_logs`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `interaction_logs` ( + `id` int NOT NULL AUTO_INCREMENT, + `session_id` int DEFAULT NULL, + `session_title` text NOT NULL, + `app_user_id` int DEFAULT NULL, + `status` ENUM('Active', 'Inactive') NOT NULL DEFAULT 'Active', + `query` text NOT NULL, + `response` text NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `hospital_code` varchar(12) NOT NULL, + PRIMARY KEY (`id`), + KEY `session_id` (`session_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `interaction_logs` +-- + +LOCK TABLES `interaction_logs` WRITE; +/*!40000 ALTER TABLE `interaction_logs` DISABLE KEYS */ +; +/*!40000 ALTER TABLE `interaction_logs` ENABLE KEYS */ +; +UNLOCK TABLES; + +-- +-- Table structure for table `onboarding_steps` +-- + +DROP TABLE IF EXISTS `onboarding_steps`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `onboarding_steps` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int DEFAULT NULL, + `step` enum( + 'Pending', + 'PasswordChanged', + 'AssetsUploaded', + 'ColorUpdated', + 'Completed' + ) DEFAULT 'Pending', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + CONSTRAINT `onboarding_steps_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `hospital_users` (`id`) +) ENGINE = InnoDB AUTO_INCREMENT = 22 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `onboarding_steps` +-- + +-- +-- Table structure for table `qa_runtime_cache` +-- + +DROP TABLE IF EXISTS `qa_runtime_cache`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `qa_runtime_cache` ( + `id` int NOT NULL AUTO_INCREMENT, + `hospital_id` int DEFAULT NULL, + `query` text NOT NULL, + `generated_answer` text, + `cached_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `hospital_id` (`hospital_id`), + CONSTRAINT `qa_runtime_cache_ibfk_1` FOREIGN KEY (`hospital_id`) REFERENCES `hospitals` (`id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `qa_runtime_cache` +-- + +LOCK TABLES `qa_runtime_cache` WRITE; +/*!40000 ALTER TABLE `qa_runtime_cache` DISABLE KEYS */ +; +/*!40000 ALTER TABLE `qa_runtime_cache` ENABLE KEYS */ +; +UNLOCK TABLES; +-- +-- Table structure for table `questions_answers` +-- + +DROP TABLE IF EXISTS `questions_answers`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `questions_answers` ( + `id` int NOT NULL AUTO_INCREMENT, + `document_id` int DEFAULT NULL, + `question` text NOT NULL, + `answer` text NOT NULL, + `type` enum('Text', 'Graph', 'Image', 'Chart') DEFAULT 'Text', + `views` INT DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `document_id` (`document_id`), + CONSTRAINT `questions_answers_ibfk_1` FOREIGN KEY (`document_id`) REFERENCES `documents` (`id`) +) ENGINE = InnoDB AUTO_INCREMENT = 489 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Table structure for table `roles` +-- + +DROP TABLE IF EXISTS `roles`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `roles` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` enum('Spurrinadmin', 'Superadmin', 'Admin', 'Viewer') NOT NULL, + `description_role` text, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + UNIQUE KEY `name_2` (`name`), + UNIQUE KEY `name_3` (`name`) +) ENGINE = InnoDB AUTO_INCREMENT = 10 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `roles` +-- + +LOCK TABLES `roles` WRITE; +/*!40000 ALTER TABLE `roles` DISABLE KEYS */ +; +INSERT INTO `roles` +VALUES ( + 6, + 'Spurrinadmin', + 'Spurrin admin access', + '2025-01-22 06:48:58', + '2025-01-22 06:48:58' + ), +( + 7, + 'Superadmin', + 'Administrator with access to manage all functionalities of a hospital including managing hospital assets.', + '2025-01-22 06:48:58', + '2025-01-22 06:48:58' + ), +( + 8, + 'Admin', + 'Administrator with access to manage all functionalities of a hospital.', + '2025-01-22 06:48:58', + '2025-01-22 06:48:58' + ), +( + 9, + 'Viewer', + 'User with read-only access.', + '2025-01-22 06:48:58', + '2025-01-22 06:48:58' + ); +/*!40000 ALTER TABLE `roles` ENABLE KEYS */ +; +UNLOCK TABLES; +-- +-- Table structure for table `super_admins` +-- + +DROP TABLE IF EXISTS `super_admins`; +/*!40101 SET @saved_cs_client = @@character_set_client */ +; +/*!50503 SET character_set_client = utf8mb4 */ +; +CREATE TABLE `super_admins` ( + `id` int NOT NULL AUTO_INCREMENT, + `email` varchar(255) NOT NULL, + `hash_password` varchar(255) DEFAULT NULL, + `role_id` int DEFAULT NULL, + `expires_at` DATETIME DEFAULT NULL, + `type` VARCHAR(50) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `refresh_token` text, + `access_token` varchar(500) DEFAULT NULL, + `access_token_expiry` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `fk_super_admin_role_id` (`role_id`), + CONSTRAINT `fk_super_admin_role_id` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) +) ENGINE = InnoDB AUTO_INCREMENT = 15 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */ +; +-- +-- Dumping data for table `super_admins` +-- + + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */ +; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */ +; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */ +; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */ +; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */ +; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */ +; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */ +; +-- Dump completed on 2025-02-21 10:37:45 + +CREATE TABLE feedback ( + feedback_id INT AUTO_INCREMENT PRIMARY KEY, + sender_type ENUM('appuser', 'hospital') NOT NULL, -- Sender type + sender_id INT NOT NULL, + receiver_type ENUM('hospital', 'spurrin') NOT NULL, -- Receiver type + receiver_id INT NOT NULL, + rating ENUM('Terrible', 'Bad', 'Okay', 'Good', 'Awesome') NOT NULL, -- Emoji satisfaction + purpose TEXT NOT NULL, -- Dynamic purpose of use + information_received ENUM('Yes', 'Partially', 'No') NOT NULL, -- Info satisfaction + feedback_text TEXT, -- Optional detailed feedback + improvement TEXT, -- What can be improved? + -- contact_for_followup ENUM('Yes', 'No') DEFAULT 'No', -- Willingness for follow-up contact + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/src/services/analysisService.js b/src/services/analysisService.js new file mode 100644 index 0000000..78d4023 --- /dev/null +++ b/src/services/analysisService.js @@ -0,0 +1,302 @@ +const db = require('../config/database'); + +class AnalysisService { + async getOnboardedHospitalsAnalysis() { + // Query 1: Get all hospital details + const hospitalDetailsQuery = ` + SELECT + h.id, + h.name_hospital AS hospital_name, + h.admin_name, + h.subdomain, + h.hospital_code, + h.status, + h.onboarding_status, + h.mobile_number, + h.created_at, + h.location, + COUNT(DISTINCT au.id) AS total_app_users, + COUNT(DISTINCT hu.id) AS total_hospital_users + FROM hospitals h + LEFT JOIN app_users au ON h.hospital_code = au.hospital_code + LEFT JOIN hospital_users hu ON h.hospital_code = hu.hospital_code + GROUP BY h.id + ORDER BY h.created_at DESC; + `; + + // Query 2: Get total onboarding stats + const onboardingStatsQuery = ` + SELECT + COUNT(*) AS total_hospitals, + COUNT(CASE WHEN onboarding_status = 'Completed' THEN 1 END) AS total_onboarded, + COUNT(CASE WHEN onboarding_status != 'Completed' THEN 1 END) AS total_not_onboarded + FROM hospitals; + `; + + // Query 3: Get active/inactive hospital counts + const statusStatsQuery = ` + SELECT + COUNT(CASE WHEN status = 'Active' THEN 1 END) AS total_active, + COUNT(CASE WHEN status = 'Inactive' THEN 1 END) AS total_inactive + FROM hospitals; + `; + + // Query 4: Get inactive hospitals only + const inactiveHospitalsQuery = ` + SELECT + id, + name_hospital AS hospital_name, + admin_name, + subdomain, + hospital_code, + status, + onboarding_status, + mobile_number, + created_at, + location + FROM hospitals + WHERE status = 'Inactive' + ORDER BY created_at DESC; + `; + + const [hospitals, onboardingStatsResult, statusStatsResult, inactiveHospitals] = await Promise.all([ + db.query(hospitalDetailsQuery), + db.query(onboardingStatsQuery), + db.query(statusStatsQuery), + db.query(inactiveHospitalsQuery) + ]); + + const onboardingStats = onboardingStatsResult[0] || { + total_hospitals: 0, + total_onboarded: 0, + total_not_onboarded: 0 + }; + + const statusStats = statusStatsResult[0] || { + total_active: 0, + total_inactive: 0 + }; + + const onboarding_pending_percentage = + onboardingStats.total_hospitals > 0 + ? parseFloat(((onboardingStats.total_not_onboarded / onboardingStats.total_hospitals) * 100).toFixed(2)) + : 0; + + return { + total_hospitals: onboardingStats.total_hospitals, + total_onboarded: onboardingStats.total_onboarded, + total_not_onboarded: onboardingStats.total_not_onboarded, + total_onboarded_hospitals: onboardingStats.total_onboarded, + onboarding_pending_percentage, + total_active: statusStats.total_active, + total_inactive: statusStats.total_inactive, + hospitals: hospitals.map(hospital => ({ + id: hospital.id, + hospital_name: hospital.hospital_name, + admin_name: hospital.admin_name, + subdomain: hospital.subdomain, + hospital_code: hospital.hospital_code, + status: hospital.status, + onboarding_status: hospital.onboarding_status, + location: hospital.location ?? null, + total_hospital_users: hospital.total_hospital_users, + contact_number: hospital.mobile_number, + total_app_users: hospital.total_app_users, + created_at: hospital.created_at + })), + inactive_hospitals: inactiveHospitals.map(hospital => ({ + id: hospital.id, + hospital_name: hospital.hospital_name, + admin_name: hospital.admin_name, + subdomain: hospital.subdomain, + hospital_code: hospital.hospital_code, + status: hospital.status, + onboarding_status: hospital.onboarding_status, + location: hospital.location ?? null, + contact_number: hospital.mobile_number, + created_at: hospital.created_at + })) + }; + } + + async getActiveHospitalsAnalysis(start_date, end_date) { + const query = ` + SELECT + h.id, + h.name_hospital as hospital_name, + h.hospital_code, + h.status, + h.onboarding_status, + COUNT(DISTINCT au.id) as total_app_users, + COUNT(DISTINCT il.id) as total_interactions, + MAX(il.created_at) as last_interaction_date + FROM hospitals h + LEFT JOIN app_users au ON h.hospital_code = au.hospital_code + LEFT JOIN interaction_logs il ON h.hospital_code = il.hospital_code + WHERE h.onboarding_status = 'completed' + ${start_date && end_date ? 'AND (il.created_at BETWEEN ? AND ? OR il.id IS NULL)' : ''} + GROUP BY h.id + HAVING total_interactions > 0 OR onboarding_status = 'completed' + ORDER BY total_interactions DESC + `; + + const params = start_date && end_date ? [start_date, end_date] : []; + const hospitals = await db.query(query, params); + + const totalCount = hospitals.length; + const totalAppUsers = hospitals.reduce((sum, hospital) => sum + hospital.total_app_users, 0); + + return { + total_active_hospitals: totalCount, + total_app_users: totalAppUsers, + period: { start_date, end_date }, + hospitals: hospitals.map(hospital => ({ + id: hospital.id, + hospital_name: hospital.hospital_name, + hospital_code: hospital.hospital_code, + status: hospital.status, + onboarding_status: hospital.onboarding_status, + total_app_users: hospital.total_app_users, + total_interactions: hospital.total_interactions, + last_interaction_date: hospital.last_interaction_date + })) + }; + } + + async getActiveChatUsersAnalysis(start_date, end_date) { + const query = ` + SELECT + au.id as user_id, + au.username, + au.email, + au.hospital_code, + h.name_hospital as hospital_name, + COUNT(il.id) as total_interactions, + MAX(il.created_at) as last_interaction_date, + MIN(il.created_at) as first_interaction_date + FROM app_users au + JOIN hospitals h ON au.hospital_code = h.hospital_code + JOIN interaction_logs il ON au.id = il.app_user_id + ${start_date && end_date ? 'WHERE il.created_at BETWEEN ? AND ?' : ''} + GROUP BY au.id + HAVING total_interactions > 0 + ORDER BY total_interactions DESC + `; + + const params = start_date && end_date ? [start_date, end_date] : []; + const activeUsers = await db.query(query, params); + + const totalCount = activeUsers.length; + const totalInteractions = activeUsers.reduce((sum, user) => sum + user.total_interactions, 0); + + return { + total_active_users: totalCount, + total_interactions: totalInteractions, + period: { start_date, end_date }, + users: activeUsers.map(user => ({ + user_id: user.user_id, + username: user.username, + email: user.email, + hospital_code: user.hospital_code, + hospital_name: user.hospital_name, + total_interactions: user.total_interactions, + last_interaction_date: user.last_interaction_date, + first_interaction_date: user.first_interaction_date + })) + }; + } + + async getHospitalRegisteredUsers(hospitalId, start_date, end_date) { + const query = ` + SELECT + h.id, + h.name_hospital as hospital_name, + h.hospital_code, + h.status, + h.onboarding_status, + COUNT(DISTINCT hu.id) as total_hospital_users, + MAX(hu.created_at) as latest_registration_date + FROM hospitals h + LEFT JOIN hospital_users hu ON h.hospital_code = hu.hospital_code + WHERE h.id = ? + ${start_date && end_date ? 'AND hu.created_at BETWEEN ? AND ?' : ''} + GROUP BY h.id + `; + + const params = [hospitalId]; + if (start_date && end_date) params.push(start_date, end_date); + + const hospitals = await db.query(query, params); + + if (hospitals.length === 0) { + throw new Error("Hospital not found"); + } + + const hospital = hospitals[0]; + + return { + hospital: { + id: hospital.id, + hospital_name: hospital.hospital_name, + hospital_code: hospital.hospital_code, + status: hospital.status, + onboarding_status: hospital.onboarding_status, + total_hospital_users: hospital.total_hospital_users, + latest_registration_date: hospital.latest_registration_date + } + }; + } + + async getHospitalActiveUsers(start_date, end_date) { + const query = ` + SELECT + h.id, + h.name_hospital as hospital_name, + h.hospital_code, + h.status, + h.onboarding_status, + COUNT(DISTINCT au.id) as total_registered_users, + COUNT(DISTINCT CASE WHEN il.id IS NOT NULL THEN au.id END) as active_users, + COUNT(DISTINCT il.id) as total_interactions, + MAX(il.created_at) as last_interaction_date + FROM hospitals h + LEFT JOIN app_users au ON h.hospital_code = au.hospital_code + LEFT JOIN interaction_logs il ON au.id = il.app_user_id + ${start_date && end_date ? 'AND il.created_at BETWEEN ? AND ?' : ''} + GROUP BY h.id + ORDER BY active_users DESC + `; + + const params = start_date && end_date ? [start_date, end_date] : []; + const hospitals = await db.query(query, params); + + const totalCount = hospitals.length; + const totalRegisteredUsers = hospitals.reduce((sum, hospital) => sum + hospital.total_registered_users, 0); + const totalActiveUsers = hospitals.reduce((sum, hospital) => sum + hospital.active_users, 0); + const totalInteractions = hospitals.reduce((sum, hospital) => sum + hospital.total_interactions, 0); + + return { + total_hospitals: totalCount, + total_registered_users: totalRegisteredUsers, + total_active_users: totalActiveUsers, + total_interactions: totalInteractions, + period: { start_date, end_date }, + hospitals: hospitals.map(hospital => ({ + id: hospital.id, + hospital_name: hospital.hospital_name, + hospital_code: hospital.hospital_code, + status: hospital.status, + onboarding_status: hospital.onboarding_status, + total_registered_users: hospital.total_registered_users, + active_users: hospital.active_users, + total_interactions: hospital.total_interactions, + last_interaction_date: hospital.last_interaction_date, + engagement_rate: hospital.total_registered_users > 0 + ? ((hospital.active_users / hospital.total_registered_users) * 100).toFixed(2) + : 0 + })) + }; + } +} + +module.exports = new AnalysisService(); \ No newline at end of file diff --git a/src/services/appUserService.js b/src/services/appUserService.js new file mode 100644 index 0000000..eea1da0 --- /dev/null +++ b/src/services/appUserService.js @@ -0,0 +1,161 @@ +const bcrypt = require("bcrypt"); +const db = require("../config/database"); +const jwt = require("jsonwebtoken"); +const nlp = require("compromise"); +const { emitEvent } = require("./secondaryWebsocket"); +const transporter = require('../config/emailConfig'); +const generatePasswordResetEmail = require('../templates/passwordResetEmail'); + +class AppUserService { + async uploadIdPhoto(userId, hospitalCode, photoPath) { + const query = ` + SELECT hospital_code FROM app_users WHERE id = ? AND hospital_code = ? + `; + const result = await db.query(query, [userId, hospitalCode]); + + if (result.length === 0) { + throw new Error("You are not authorized to upload ID for this user"); + } + + await db.query("UPDATE app_users SET id_photo_url = ? WHERE id = ?", [ + photoPath, + userId, + ]); + + return photoPath; + } + + async updateSettings(userId, { pin, pin_enabled, remember_me }) { + if (pin && (typeof pin !== 'string' || pin.length !== 4)) { + throw new Error('Invalid PIN format. Must be a 4-digit string.'); + } + + let expiresIn = '5h'; + let expiryTimestamp = new Date(); + + if (remember_me) { + expiresIn = '30d'; + expiryTimestamp.setDate(expiryTimestamp.getDate() + 30); + } else { + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + } + + const payload = { id: userId, role: 'AppUser' }; + const accessToken = jwt.sign(payload, process.env.JWT_ACCESS_TOKEN_SECRET, { + expiresIn: expiresIn, + }); + + const query = `UPDATE app_users SET pin_number = ?, pin_enabled = ?, remember_me = ?, access_token = ?, access_token_expiry = ? WHERE id = ?`; + const result = await db.query(query, [pin, pin_enabled, remember_me, accessToken, expiryTimestamp, userId]); + + if (result.affectedRows === 0) { + throw new Error("User not found"); + } + + return { accessToken }; + } + + async toggleLike(app_user_id, session_id, log_id) { + const toggleQuery = ` + UPDATE interaction_logs + SET is_liked = NOT is_liked + WHERE app_user_id = ? AND session_id = ? AND id = ? + `; + + const result = await db.query(toggleQuery, [app_user_id, session_id, log_id]); + + if (result.affectedRows === 0) { + throw new Error('No matching record found to toggle'); + } + + return { + app_user_id, + session_id, + is_liked: result.changedRows > 0 ? 1 : 0 + }; + } + + async signup({ email, password, hospital_code, username, pin, pin_status, remember_me }) { + if (!email || !password || !hospital_code || !username) { + throw new Error("Email, password, username, and hospital code are required"); + } + + const pin_enabled = (pin_status === undefined || pin_status === '') ? 0 : pin_status; + const remember_me_ = (remember_me === undefined || remember_me === '') ? 0 : remember_me; + + const hospitalQuery = "SELECT hospital_code FROM hospitals WHERE hospital_code = ?"; + const hospitalResult = await db.query(hospitalQuery, [hospital_code]); + + if (hospitalResult.length === 0) { + throw new Error("Invalid hospital code"); + } + + const userQuery = "SELECT id FROM app_users WHERE email = ?"; + const userResult = await db.query(userQuery, [email]); + + if (userResult.length > 0) { + throw new Error("Email already in use"); + } + + const hashPassword = await bcrypt.hash(password, 10); + + const insertQuery = ` + INSERT INTO app_users (email, hash_password, hospital_code, status, username, pin_number, pin_enabled, remember_me) + VALUES (?, ?, ?, 'Pending', ?, ?, ?, ?) + `; + const result = await db.query(insertQuery, [ + email, + hashPassword, + hospital_code, + username, + pin, + pin_enabled, + remember_me_ + ]); + + return result.insertId; + } + + async sendMail(email, hospital_name, username, otp) { + const mailOptions = { + from: process.env.EMAIL_USER, + to: email, + subject: 'Password Reset Request', + html: generatePasswordResetEmail(hospital_name, username, otp) + }; + + await transporter.sendMail(mailOptions); + } + + async getMappedPopularQuestionsAnswers(hospitalCode) { + console.log("Hospital code is---", hospitalCode); + + try { + const query = ` + SELECT il.query, il.response + FROM interaction_logs il + WHERE il.hospital_code = ? + ORDER BY il.created_at DESC -- Sorting by most recent interactions + LIMIT 10; -- Fetch more than 4 to filter unwanted entries + `; + + const rows = await db.query(query, [hospitalCode]); + + console.log("Fetched questions before filtering:", rows); + + // Skip the row if either condition is true + const filteredRows = rows.filter(row => + !(row.query.toLowerCase().includes("yes") || row.response.includes("Please reply with 'yes'")) + ); + + // Return only the top 4 filtered results + return filteredRows.slice(0, 4); + + } catch (error) { + console.error("Error fetching popular topics:", error.message); + throw new Error("Internal server error"); + } + } +} + +module.exports = new AppUserService(); \ No newline at end of file diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..d2ba22c --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,220 @@ +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); +const db = require("../config/database"); +const JWT_ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_TOKEN_SECRET; +const JWT_REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_TOKEN_SECRET; +const JWT_ACCESS_TOKEN_EXPIRY = process.env.JWT_ACCESS_TOKEN_EXPIRY || "5h"; + +class AuthService { + async logout(token) { + const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); + let table; + + if (decoded.role === "Spurrinadmin") { + table = "super_admins"; + } else if (["Admin", "Viewer", "Superadmin", 8, 9].includes(decoded.role)) { + table = "hospital_users"; + } else if (decoded.role === "AppUser") { + table = "app_users"; + } + + const id = decoded.id; + await db.query(`UPDATE ${table} SET access_token = NULL WHERE id = ?`, [id]); + return { message: "Logout successful!" }; + } + + async refreshToken(refreshToken, user_id) { + const decoded = jwt.verify(refreshToken, JWT_REFRESH_TOKEN_SECRET); + const { role } = decoded; + + const query = ` + SELECT id, email, role_id, refresh_token + FROM super_admins + WHERE id = ? AND refresh_token = ? + `; + const result = await db.query(query, [user_id, refreshToken]); + + if (result.length === 0) { + throw new Error("Invalid or expired refresh token"); + } + + const user = result[0]; + const payload = { id: user.id, email: user.email, role }; + const newAccessToken = jwt.sign(payload, JWT_ACCESS_TOKEN_SECRET, { + expiresIn: JWT_ACCESS_TOKEN_EXPIRY, + }); + + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + const updateQuery = ` + UPDATE super_admins + SET access_token = ?, access_token_expiry = ? + WHERE id = ? + `; + await db.query(updateQuery, [newAccessToken, expiryTimestamp, user_id]); + + return { + message: "Access token generated and updated successfully", + accessToken: newAccessToken, + user_id: user.id + }; + } + + async login(providedAccessToken, email, password) { + let decoded; + try { + decoded = jwt.verify(providedAccessToken, JWT_ACCESS_TOKEN_SECRET); + } catch (err) { + throw new Error("Invalid or expired access token"); + } + + const { id, role } = decoded; + let table = role === "Spurrinadmin" ? "super_admins" : "hospital_users"; + + let userQuery = `SELECT * FROM ${table} WHERE id = ?`; + const userResult = await db.query(userQuery, [id]); + const user = userResult[0]; + + if (!user) { + throw new Error("Unauthorized access"); + } + + if (user.access_token !== providedAccessToken) { + throw new Error("Invalid or expired access token"); + } + + const now = new Date(); + const expiryDate = new Date(user.access_token_expiry); + if (now > expiryDate) { + throw new Error("Access token has expired"); + } + + const validPassword = await bcrypt.compare( + password, + user.hash_password || user.password + ); + if (!validPassword) { + throw new Error("Invalid email or password"); + } + + const payload = { id: user.id, email: user.email, role }; + const newAccessToken = jwt.sign(payload, JWT_ACCESS_TOKEN_SECRET, { + expiresIn: "5h", + }); + + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + const updateQuery = ` + UPDATE ${table} + SET access_token = ?, access_token_expiry = ? + WHERE id = ? + `; + await db.query(updateQuery, [newAccessToken, expiryTimestamp, user.id]); + + let Role; + if (user.role_id == 6) { + Role = "Spurrinadmin"; + } else if (user.role_id == 7) { + Role = "Superadmin"; + } else if (user.role_id == 8) { + Role = "Admin"; + } else if (user.role_id == 9) { + Role = "Viewer"; + } else { + Role = "AppUser"; + } + + const response = { + message: "Login successful", + user: { + id: user.id, + email: user.email, + role: Role, + status: user.status, + }, + accessToken: newAccessToken, + }; + + if (table === "hospital_users") { + const subdomain_data = user; + const hospitalQuery = `SELECT * FROM hospitals WHERE hospital_code = ?`; + const hospitalResult = await db.query(hospitalQuery, [subdomain_data.hospital_code]); + const hospitalData = hospitalResult[0]; + const subdomain = hospitalResult.length > 0 ? hospitalResult[0].subdomain : null; + + response.user.hospital_id = user.hospital_id; + response.name = user.name; + response.profile_photo_url = subdomain_data.profile_photo_url; + response.subdomain = subdomain; + response.primary_color = hospitalData.primary_color; + response.secondary_color = hospitalData.secondary_color; + response.password_reset_required = subdomain_data.password_reset_required; + response.hospital_name = hospitalData.name_hospital; + } + + return response; + } + + async authenticateToken(token, hospital_id) { + const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); + const { id, role } = decoded; + + if (isNaN(hospital_id)) { + throw new Error("Invalid hospital ID"); + } + + const query = ` + SELECT id, hospital_id, access_token, access_token_expiry, role_id + FROM hospital_users + WHERE hospital_id = ? AND access_token = ? + `; + + const result = await db.query(query, [hospital_id, token]); + const user = result[0]; + + if (!user) { + throw new Error("You are not authorized to access this hospital"); + } + + const now = new Date(); + const expiryDate = new Date(user.access_token_expiry); + if (now > expiryDate) { + throw new Error("Access token has expired"); + } + + return { + id: user.id, + hospital_id: user.hospital_id, + role, + }; + } + + async checkAccessToken(token) { + const decoded = jwt.decode(token); + const id = decoded.id; + let table; + + if (decoded.role === "Spurrinadmin") { + table = "super_admins"; + } else if (["Admin", "Viewer", "Superadmin", 7, 8, 9].includes(decoded.role)) { + table = "hospital_users"; + } else if (decoded.role === "AppUser") { + table = "app_users"; + } + + const result = await db.query( + `SELECT access_token FROM ${table} WHERE id = ?`, + [id] + ); + + if (result.length > 0 && result[0].access_token === token) { + return { message: "Token is active" }; + } else { + return { message: "Token not found or mismatched" }; + } + } +} + +module.exports = new AuthService(); \ No newline at end of file diff --git a/src/services/cronJobs.js b/src/services/cronJobs.js new file mode 100644 index 0000000..4cc0bd8 --- /dev/null +++ b/src/services/cronJobs.js @@ -0,0 +1,167 @@ +// const cron = require('node-cron'); +// const jwt = require('jsonwebtoken'); +// const db = require('../config/database'); // Database connection + +// // Generate a new refresh token +// const generateRefreshToken = (id, email, role) => { + +// const generateRefreshToken = (id, email, role_id) => { +// // Map role_id to role name (e.g., Spurrinadmin, Superadmin) +// const roleMap = { +// 6: 'Spurrinadmin', +// 7: 'Superadmin', +// 8: 'Admin', // Adjust as needed +// 9: 'Viewer', +// }; + +// const role = roleMap[role_id] || 'UnknownRole'; +// return jwt.sign({ id, email, role }, process.env.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' }); +// }; + +// return jwt.sign({ id, email, role }, process.env.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' }); +// }; + +// // Function to update expired refresh tokens +// const refreshExpiredTokens = async () => { +// try { +// console.log("🔄 Running Refresh Token Renewal Cron Job..."); + +// // Check both `super_admins` and `hospital_users` for expiring refresh tokens +// const tables = ['super_admins', 'hospital_users']; + +// for (const table of tables) { +// const query = `SELECT id, email, role_id, refresh_token FROM ${table} WHERE refresh_token IS NOT NULL`; +// const result = await db.query(query); + +// // **Fix: Ensure the query result is properly formatted** +// let users = []; +// if (Array.isArray(result)) { +// users = result.length > 0 ? result : []; +// } else if (Array.isArray(result[0])) { +// users = result[0]; // Handle case where MySQL returns nested array +// } else { +// console.error(`❌ Unexpected query result format for ${table}:`, result); +// continue; // Skip to next table if unexpected format +// } + +// for (const user of users) { +// try { +// jwt.verify(user.refresh_token, process.env.JWT_REFRESH_TOKEN_SECRET); +// } catch (err) { +// if (err.name === 'TokenExpiredError') { +// console.log(`🔄 Refresh Token Expired for User ID: ${user.id} in ${table}`); + +// // Generate a new refresh token +// const newRefreshToken = generateRefreshToken(user.id, user.email, user.role_id); + +// // Update the database with the new refresh token +// const updateQuery = `UPDATE ${table} SET refresh_token = ? WHERE id = ?`; +// await db.query(updateQuery, [newRefreshToken, user.id]); + +// console.log(`✅ New Refresh Token Generated for User ID: ${user.id} in ${table}`); +// } +// } +// } +// } +// } catch (error) { +// console.error("❌ Error refreshing expired tokens:", error.message); +// } +// }; + +// // Schedule the task to run every 1 hour +// cron.schedule('0 * * * *', async () => { +// await refreshExpiredTokens(); +// console.log("🔄 Refresh Token Cron Job Executed Successfully!"); +// }); + +// module.exports = { refreshExpiredTokens }; + + +const cron = require('node-cron'); +const jwt = require('jsonwebtoken'); +const db = require('../config/database'); // Database connection + +// Generate a new refresh token +const generateRefreshToken = (id, email, role_id) => { + const roleMap = { + 6: 'Spurrinadmin', + 7: 'Superadmin', + 8: 'Admin', + 9: 'Viewer', + }; + const role = roleMap[role_id] || 'UnknownRole'; + + console.log("role-----",role) + return jwt.sign( + { id, email, role }, + process.env.JWT_REFRESH_TOKEN_SECRET, + { expiresIn: '7d' } // You can change to '30d' if needed + ); +}; + +// Function to update expired or near-expiry refresh tokens +const refreshExpiredTokens = async () => { + try { + console.log("🔄 Running Refresh Token Renewal Cron Job..."); + + const tables = ['super_admins', 'hospital_users']; + + for (const table of tables) { + try { + const query = `SELECT id, email, role_id, refresh_token FROM ${table} WHERE refresh_token IS NOT NULL`; + const result = await db.query(query); + + let users = Array.isArray(result) ? result : Array.isArray(result[0]) ? result[0] : []; + if (users.length === 0) { + console.log(`⚠️ No refresh tokens found in ${table}.`); + continue; + } + + for (const user of users) { + try { + const decoded = jwt.verify(user.refresh_token, process.env.JWT_REFRESH_TOKEN_SECRET, { ignoreExpiration: true }); + const currentTime = Math.floor(Date.now() / 1000); + const timeToExpire = decoded.exp - currentTime; + + if (timeToExpire < 3600) { // Less than 1 hour remaining + console.log(`🔄 Refresh Token Near Expiry for User ID: ${user.id} in ${table}`); + + const newRefreshToken = generateRefreshToken(user.id, user.email, user.role_id); + + const updateQuery = `UPDATE ${table} SET refresh_token = ? WHERE id = ?`; + await db.query(updateQuery, [newRefreshToken, user.id]); + + console.log(`✅ New Refresh Token Generated for User ID: ${user.id} in ${table}, Expires At: ${new Date((decoded.exp + 7 * 24 * 60 * 60) * 1000).toISOString()}`); + } + } catch (err) { + if (err.name === 'TokenExpiredError') { + console.log(`🔴 Refresh Token Expired for User ID: ${user.id} in ${table}`); + + const newRefreshToken = generateRefreshToken(user.id, user.email, user.role_id); + const updateQuery = `UPDATE ${table} SET refresh_token = ? WHERE id = ?`; + await db.query(updateQuery, [newRefreshToken, user.id]); + + console.log(`✅ New Refresh Token Generated for Expired Token - User ID: ${user.id} in ${table}`); + } else { + console.error(`❌ Invalid Refresh Token for User ID: ${user.id} in ${table}:`, err.message); + } + } + } + } catch (queryError) { + console.error(`❌ Failed to process table ${table}:`, queryError.message); + } + } + } catch (error) { + console.error("❌ Unexpected error in refresh token cron job:", error.message); + } +}; + +// Schedule the task to run every hour +cron.schedule('0 * * * *', async () => { + await refreshExpiredTokens(); + console.log("🔄 Refresh Token Cron Job Executed Successfully!"); +}); + +module.exports = { refreshExpiredTokens }; + + \ No newline at end of file diff --git a/src/services/exceldataService.js b/src/services/exceldataService.js new file mode 100644 index 0000000..fc7aaa2 --- /dev/null +++ b/src/services/exceldataService.js @@ -0,0 +1,125 @@ +const db = require('../config/database'); +const back_url = process.env.BACK_URL; +const jwt = require("jsonwebtoken"); +const bcrypt = require('bcrypt'); +const transporter = require('../config/emailConfig'); +const generateWelcomeEmail = require('../templates/welcomeEmail'); + +class ExcelDataService { + async createExcelEntry(hospital_id, hospital_code, requestorRole, data) { + if (!['Superadmin', 'Admin', 8, 7].includes(requestorRole)) { + throw new Error('Access denied. Only Superadmin and Admin can do this action.'); + } + + if (!Array.isArray(data)) { + throw new Error("Invalid data format. Expected an array."); + } + + // Get hospital user details + const hospitalUsersQuery = ` + SELECT * + FROM hospital_users + WHERE hospital_id = ? + `; + const hospitalUserResult = await db.query(hospitalUsersQuery, [hospital_id]); + + if (!hospitalUserResult || hospitalUserResult.length === 0) { + throw new Error('Hospital not found for the given hospital_id'); + } + + // Get hospital details + const hospitalQuery = ` + SELECT * + FROM hospitals + WHERE hospital_code = ? + `; + const hospitalResult = await db.query(hospitalQuery, [hospital_code]); + + // Send welcome emails + await this.sendEmails(data, hospitalResult, back_url); + + // Prepare data for insertion + const query = ` + INSERT INTO hospital_users + (hospital_code, hospital_id, email, hash_password, role_id, is_default_admin, requires_onboarding, + password_reset_required, profile_photo_url, phone_number, bio, status, name, department, location, mobile_number) + VALUES ? + `; + + const values_hospital_users = await Promise.all(data.map(async (item) => { + const hashedPassword = await bcrypt.hash(item.password, 10); + return [ + hospital_code, + hospital_id, + item.email, + hashedPassword, + item.role, + 0, + hospitalUserResult[0].requires_onboarding, + hospitalUserResult[0].password_reset_required, + hospitalUserResult[0].profile_photo_url, + item.phonenumber, + hospitalUserResult[0].bio, + hospitalUserResult[0].status, + item.name, + item.department, + item.location, + item.phonenumber + ]; + })); + + const result = await db.query(query, [values_hospital_users]); + + // Generate and update refresh tokens for each inserted user + const firstInsertedId = result.insertId; + const numberOfInsertedRows = result.affectedRows; + + await Promise.all( + data.map(async (item, index) => { + const insertedUserId = firstInsertedId + index; + const refreshTokenPayload = { + id: insertedUserId, + email: item.email, + role: item.role, + }; + + const refreshToken = jwt.sign( + refreshTokenPayload, + process.env.JWT_REFRESH_TOKEN_SECRET + ); + + const updateRefreshTokenQuery = `UPDATE hospital_users SET refresh_token = ? WHERE id = ?`; + await db.query(updateRefreshTokenQuery, [refreshToken, insertedUserId]); + }) + ); + + return { message: "Records added successfully!" }; + } + + async sendEmails(users, hospitalResult, back_url) { + for (const user of users) { + const mailOptions = { + from: process.env.EMAIL_USER, + to: user.email, + subject: 'Spurrinai Login Credentials', + html: generateWelcomeEmail( + user.email, + user.name, + hospitalResult[0].subdomain, + user.password, + hospitalResult[0].name_hospital, + back_url + ) + }; + + try { + await transporter.sendMail(mailOptions); + } catch (error) { + console.error(`Error sending email to ${user.email}:`, error); + // Continue with other emails even if one fails + } + } + } +} + +module.exports = new ExcelDataService(); \ No newline at end of file diff --git a/src/services/feedbacksService.js b/src/services/feedbacksService.js new file mode 100644 index 0000000..fc2f693 --- /dev/null +++ b/src/services/feedbacksService.js @@ -0,0 +1,351 @@ +const db = require('../config/database'); + +class FeedbacksService { + async createAppUserFeedback(user_id, hospital_code, feedbackData) { + const { + rating, + purpose, + information_received, + feedback_text, + improvement, + } = feedbackData; + + if (!hospital_code) { + throw new Error('Hospital code is required'); + } + + // Set default values if not provided + const validRating = ['Terrible', 'Bad', 'Okay', 'Good', 'Awesome']; + const validInfoReceived = ['Yes', 'Partially', 'No']; + + const finalRating = rating && validRating.includes(rating) ? rating : null; + const finalInfoReceived = information_received && validInfoReceived.includes(information_received) + ? information_received + : null; + + // Check if hospital exists + const hospitalCheck = await db.query( + 'SELECT id FROM hospitals WHERE hospital_code = ?', + [hospital_code] + ); + + if (hospitalCheck.length === 0) { + throw new Error('Hospital not found'); + } + + // Insert feedback + const query = ` + INSERT INTO feedback ( + sender_type, + sender_id, + receiver_type, + receiver_id, + rating, + purpose, + information_received, + feedback_text, + improvement + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + const result = await db.query(query, [ + 'appuser', + user_id, + 'hospital', + hospitalCheck[0].id, + finalRating, + purpose, + finalInfoReceived, + feedback_text || null, + improvement || null, + ]); + + return { + message: 'Feedback submitted successfully', + feedback_id: result.insertId, + }; + } + + async createHospitalFeedback(hospital_code, feedbackData) { + const { + rating, + purpose, + information_received, + feedback_text, + improvement + } = feedbackData; + + if (!rating || !purpose || !information_received) { + throw new Error("Rating, purpose and information received are required"); + } + + // Validate rating enum + const validRating = ['angry', 'sad', 'neutral', 'happy', 'awesome']; + if (!validRating.includes(rating)) { + throw new Error("Invalid rating value"); + } + + // Validate information_received enum + const validInfoReceived = ['Yes', 'Partially', 'No']; + if (!validInfoReceived.includes(information_received)) { + throw new Error("Invalid information received value"); + } + + // Get hospital ID + const hospitalCheck = await db.query( + 'SELECT id FROM hospitals WHERE hospital_code = ?', + [hospital_code] + ); + + if (hospitalCheck.length === 0) { + throw new Error("Hospital not found"); + } + + // Insert feedback + const query = ` + INSERT INTO feedback ( + sender_type, + sender_id, + receiver_type, + receiver_id, + rating, + purpose, + information_received, + feedback_text, + improvement + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + const result = await db.query(query, [ + 'hospital', + hospitalCheck[0].id, + 'spurrin', + 1, // Assuming 1 is the ID for Spurrin + rating, + purpose, + information_received, + feedback_text || null, + improvement || null + ]); + + return { + message: "Feedback submitted successfully", + feedback_id: result.insertId + }; + } + + async getHospitalFeedbacks(hospital_code) { + // Get hospital ID + const hospitalCheck = await db.query( + 'SELECT id FROM hospitals WHERE hospital_code = ?', + [hospital_code] + ); + + if (hospitalCheck.length === 0) { + throw new Error("Hospital not found"); + } + + const query = ` + SELECT + f.feedback_id, + f.sender_type, + f.sender_id, + f.receiver_type, + f.receiver_id, + f.rating, + f.purpose, + f.information_received, + f.feedback_text, + f.improvement, + f.created_at, + f.is_forwarded, + au.username as user_name, + au.email as user_email + FROM feedback f + LEFT JOIN app_users au ON f.sender_id = au.id AND f.sender_type = 'appuser' + WHERE f.receiver_type = 'hospital' AND f.receiver_id = ? + ORDER BY f.created_at DESC + `; + + const feedbacks = await db.query(query, [hospitalCheck[0].id]); + + return { + message: "Feedbacks fetched successfully", + data: feedbacks + }; + } + + async getAllFeedbacks(userRole) { + if (userRole !== 'Spurrinadmin' && userRole !== 6) { + throw new Error("You are not authorized!"); + } + + const query = ` + SELECT + f.feedback_id, + f.sender_type, + f.sender_id, + f.receiver_type, + f.receiver_id, + f.rating, + f.purpose, + f.information_received, + f.feedback_text, + f.created_at, + f.is_forwarded, + au.name as user_name, + au.email as user_email, + h.name_hospital as hospital_name, + h.hospital_code + FROM feedback f + LEFT JOIN app_users au ON f.sender_id = au.id AND f.sender_type = 'appuser' + LEFT JOIN hospitals h ON f.sender_id = h.id AND f.sender_type = 'hospital' + ORDER BY f.created_at DESC + `; + + const feedbacks = await db.query(query); + + return { + message: "All feedbacks fetched successfully", + data: feedbacks + }; + } + + async forwardAppUserFeedbacks(hospital_code, feedback_ids) { + if (!feedback_ids || !Array.isArray(feedback_ids) || feedback_ids.length === 0) { + throw new Error("Feedback IDs array is required"); + } + + const hospitalCheck = await db.query( + 'SELECT id FROM hospitals WHERE hospital_code = ?', + [hospital_code] + ); + + if (hospitalCheck.length === 0) { + throw new Error("Hospital not found"); + } + + const hospitalId = hospitalCheck[0].id; + + const verifyQuery = ` + SELECT feedback_id + FROM feedback + WHERE feedback_id IN (?) + AND receiver_type = 'hospital' + AND receiver_id = ? + AND sender_type = 'appuser' + `; + + const validFeedbacks = await db.query(verifyQuery, [feedback_ids, hospitalId]); + + if (validFeedbacks.length !== feedback_ids.length) { + throw new Error("One or more feedback IDs are invalid or don't belong to this hospital"); + } + + const forwardPromises = feedback_ids.map(async (feedback_id) => { + const originalFeedback = await db.query( + 'SELECT * FROM feedback WHERE feedback_id = ?', + [feedback_id] + ); + + if (originalFeedback.length === 0) return null; + const feedback = originalFeedback[0]; + + // Insert new feedback for Spurrin + await db.query(` + INSERT INTO feedback ( + sender_type, + sender_id, + receiver_type, + receiver_id, + rating, + purpose, + information_received, + feedback_text, + improvement + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + 'hospital', + hospitalId, + 'spurrin', + 1, // Spurrin ID + feedback.rating, + `Purpose: ${feedback.purpose}`, + feedback.information_received, + feedback.feedback_text, + feedback.improvement || null + ] + ); + + // Mark original feedback as forwarded + await db.query( + 'UPDATE feedback SET is_forwarded = 1 WHERE feedback_id = ?', + [feedback_id] + ); + }); + + await Promise.all(forwardPromises); + + return { + message: "Feedbacks forwarded to Spurrin successfully", + forwarded_count: feedback_ids.length + }; + } + + async getForwardedFeedbacks(userRole) { + if (userRole !== 'Spurrinadmin' && userRole !== 6) { + throw new Error("You are not authorized!"); + } + + const query = ` + SELECT + f.sender_type, + f.sender_id, + f.receiver_type, + f.receiver_id, + f.rating, + f.purpose, + f.information_received, + f.feedback_text, + f.created_at, + f.is_forwarded, + f.improvement, + h.name_hospital as sender_hospital, + h.hospital_code + FROM feedback f + LEFT JOIN hospitals h ON f.sender_id = h.id AND f.sender_type = 'hospital' + WHERE f.receiver_type = 'spurrin' + ORDER BY f.created_at DESC + `; + + const forwardedFeedbacks = await db.query(query); + + return { + message: "Forwarded feedbacks fetched successfully.", + data: forwardedFeedbacks + }; + } + + async deleteAppUserFeedback(feedbackId, userRole) { + if (!feedbackId) { + throw new Error('Feedback ID is required'); + } + + if ( + userRole !== 'Spurrinadmin' && + userRole !== 6 && + userRole !== 'Superadmin' && + userRole !== 7 + ) { + throw new Error('You are not authorized!'); + } + + await db.query('DELETE FROM feedback WHERE feedback_id = ?', [feedbackId]); + + return { + message: 'Feedback deleted successfully', + }; + } +} + +module.exports = new FeedbacksService(); \ No newline at end of file diff --git a/src/services/hospitalService.js b/src/services/hospitalService.js new file mode 100644 index 0000000..8d52092 --- /dev/null +++ b/src/services/hospitalService.js @@ -0,0 +1,841 @@ +const db = require("../config/database"); +const jwt = require("jsonwebtoken"); +const bcrypt = require("bcrypt"); +const path = require("path"); +const back_url = process.env.BACK_URL; +const fs = require("fs"); +const tokenService = require('./tokenService'); +const transporter = require('../config/emailConfig'); +const crypto = require("crypto"); +const generatePasswordResetEmail = require('../templates/passwordResetEmail'); +const generateWelcomeEmail = require('../templates/welcomeEmail'); + +class HospitalService { + generateHospitalCode() { + const length = 12; + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let code = ""; + for (let i = 0; i < length; i++) { + code += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return code; + } + + async createHospital(hospitalData, super_admin_id, incomingAccessToken) { + const { + name_hospital, + subdomain, + primary_admin_email, + primary_admin_password, + primary_color, + secondary_color, + logo_url, + admin_name, + mobile_number, + location, + } = hospitalData; + + // Check email if already exists + const spurrinEmailQuery = "SELECT email from super_admins WHERE email = ?"; + const spurrinEmailResult = await db.query(spurrinEmailQuery, [primary_admin_email]); + + if (spurrinEmailResult.length > 0) { + throw new Error("Email already exists!"); + } + + const hsptUsrEmailQuery = "SELECT email from hospital_users WHERE email = ?"; + const hsptUsrEmailResult = await db.query(hsptUsrEmailQuery, [primary_admin_email]); + + if (hsptUsrEmailResult.length > 0) { + throw new Error("Email already exists!"); + } + + // Generate a unique hospital code + let hospitalCode; + let isUnique = false; + + while (!isUnique) { + hospitalCode = this.generateHospitalCode(); + const codeExists = await db.query( + "SELECT COUNT(*) as count FROM hospitals WHERE hospital_code = ?", + [hospitalCode] + ); + if (codeExists[0].count === 0) { + isUnique = true; + } + } + + // Validate if the SuperAdmin exists + const superAdminQuery = "SELECT id, access_token FROM super_admins WHERE id = ?"; + const superAdminResult = await db.query(superAdminQuery, [super_admin_id]); + + if (superAdminResult.length === 0) { + throw new Error("Invalid super_admin_id"); + } + + const superAdmin = superAdminResult[0]; + + // Ensure the access token matches + if (superAdmin.access_token !== incomingAccessToken) { + throw new Error("Unauthorized: Access token does not match the SuperAdmin's token in the database"); + } + + // Hash the primary admin's password + const hashedPassword = await bcrypt.hash(primary_admin_password, 10); + + // Insert hospital + const insertHospitalQuery = ` + INSERT INTO hospitals ( + name_hospital, + subdomain, + primary_admin_email, + primary_admin_password, + primary_color, + secondary_color, + logo_url, + status, + onboarding_status, + admin_name, + mobile_number, + location, + super_admin_id, + hospital_code, + type + ) VALUES (?, ?, ?, ?, ?, ?, ?, 'Active', 'Pending', ?, ?, ?, ?,?,NULL) + `; + const hospitalResult = await db.query(insertHospitalQuery, [ + name_hospital, + subdomain, + primary_admin_email, + hashedPassword, + primary_color, + secondary_color, + logo_url, + admin_name, + mobile_number, + location, + super_admin_id, + hospitalCode, + ]); + + const hospitalId = hospitalResult.insertId; + + // Insert primary admin + const insertUserQuery = ` + INSERT INTO hospital_users ( + hospital_id, + email, + hash_password, + role_id, + is_default_admin, + requires_onboarding, + password_reset_required, + phone_number, + status, + hospital_code, + name, + type, + location + ) VALUES (?, ?, ?, ?, TRUE, TRUE, TRUE, ?, 'Active', ?, ?, NULL,?) + `; + + const roleId = 7; + const insertResult = await db.query(insertUserQuery, [ + hospitalId, + primary_admin_email, + hashedPassword, + roleId, + mobile_number, + hospitalCode, + admin_name, + location + ]); + + const insertedUserId = insertResult.insertId; + const payload = { id: insertedUserId, email: primary_admin_email, role: roleId }; + const newAccessToken = tokenService.generateAccessToken(payload); + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + const refreshTokenPayload = { + id: insertedUserId, + email: primary_admin_email, + role: "Superadmin", + }; + const refreshToken = jwt.sign( + refreshTokenPayload, + process.env.JWT_REFRESH_TOKEN_SECRET + ); + + const updateRefreshTokenQuery = `UPDATE hospital_users SET refresh_token = ?, access_token = ?, access_token_expiry= ? WHERE id = ?`; + await db.query(updateRefreshTokenQuery, [refreshToken, newAccessToken, expiryTimestamp, insertedUserId]); + + // Send welcome email + const mailOptions = { + from: process.env.EMAIL_USER, + to: primary_admin_email, + subject: "Spurrinai Login Credentials", + html: generateWelcomeEmail({ + primary_admin_email, + name_hospital, + subdomain, + primary_admin_password, + admin_name, + back_url: back_url, + }), + }; + + let emailInfo; + try { + const info = await transporter.sendMail(mailOptions); + emailInfo = info.response; + } catch (emailError) { + console.error("Email sending failed:", emailError.message); + emailInfo = "Email sending failed: " + emailError.message; + } + + return { + message: "Hospital and Primary SuperAdmin created successfully!", + hospital: { + id: hospitalId, + name_hospital, + subdomain, + primary_admin_email, + primary_color, + secondary_color, + logo_url, + admin_name, + mobile_number, + location, + super_admin_id, + hospitalCode + }, + refreshToken, + emailInfo + }; + } + + async getHospitalList(superAdminId) { + const query = ` + SELECT h.*, sa.email AS super_admin_email + FROM hospitals h + LEFT JOIN super_admins sa ON h.super_admin_id = sa.id + `; + const hospitals = await db.query(query); + return { + message: "Hospital list fetched successfully!", + data: hospitals, + }; + } + + async getHospitalById(id) { + const query = "SELECT * FROM hospitals WHERE id = ?"; + const result = await db.query(query, [id]); + + if (result.length === 0) { + throw new Error("Hospital not found"); + } + + return { + message: "Hospital fetched successfully!", + data: result[0], + }; + } + + async updateHospital(id, updateData, userId, userRole) { + const hospitalQuery = "SELECT id hospital_code, super_admin_id FROM hospitals WHERE id = ?"; + const hospitalResult = await db.query(hospitalQuery, [id]); + + const hospitalQueryUsr = "SELECT id FROM hospital_users WHERE hospital_id = ?"; + const hospitalResultUsr = await db.query(hospitalQueryUsr, [id]); + + if (hospitalResult.length === 0) { + throw new Error("Hospital not found"); + } + + if (userId !== hospitalResultUsr[0].id && userId !== hospitalResult[0].super_admin_id) { + throw new Error("You can only edit the hospital you have created"); + } + + const validColumns = new Set([ + "name_hospital", + "primary_admin_password", + "primary_color", + "secondary_color", + "logo_url", + "status", + "onboarding_status", + "admin_name", + "mobile_number", + "location", + "super_admin_id", + ]); + + for (const key of Object.keys(updateData)) { + if (!validColumns.has(key)) { + throw new Error(`Invalid field or cannot update: ${key}`); + } + } + + const fields = []; + const values = []; + for (const [key, value] of Object.entries(updateData)) { + fields.push(`${key} = ?`); + values.push(value); + } + + values.push(id); + + const query = `UPDATE hospitals SET ${fields.join(", ")} WHERE id = ?`; + const result = await db.query(query, values); + + if (result.affectedRows === 0) { + throw new Error("Hospital not found or no changes made"); + } + + const queryhspt = `SELECT * FROM hospitals WHERE id = ?`; + const resulthspt = await db.query(queryhspt, [id]); + + return { + message: "Hospital updated successfully!", + data: resulthspt + }; + } + + async deleteHospital(id, userId, userRole) { + if (!["Spurrinadmin", 6].includes(userRole)) { + throw new Error("You are not authorized to delete hospitals"); + } + + const hospitalQuery = "SELECT hospital_code, super_admin_id FROM hospitals WHERE id = ?"; + const hospitalResult = await db.query(hospitalQuery, [id]); + + if (hospitalResult.length === 0) { + throw new Error("Hospital not found"); + } + + if (userId !== hospitalResult[0].super_admin_id) { + throw new Error("You can only delete the hospital you have created"); + } + + // Delete associated files and records + const documents = await db.query( + "SELECT id, file_url FROM documents WHERE hospital_id = ?", + [id] + ); + + for (const document of documents) { + if (document.file_url) { + const filePath = path.join( + __dirname, + "..", + "uploads", + document.file_url.replace(/^\/uploads\//, "") + ); + + try { + await fs.promises.access(filePath, fs.constants.F_OK); + await fs.promises.unlink(filePath); + } catch (err) { + console.error(`Error deleting or accessing file ${filePath}: ${err.message}`); + } + } + } + + await db.query( + "DELETE FROM questions_answers WHERE document_id IN (SELECT id FROM documents WHERE hospital_id = ?)", + [id] + ); + + await db.query( + "DELETE FROM document_metadata WHERE document_id IN (SELECT id FROM documents WHERE hospital_id = ?)", + [id] + ); + + await db.query( + "DELETE FROM document_pages WHERE document_id IN (SELECT id FROM documents WHERE hospital_id = ?)", + [id] + ); + + await db.query( + "DELETE FROM onboarding_steps WHERE user_id IN (SELECT id from hospital_users WHERE hospital_code = ?)", + [hospitalResult[0].hospital_code] + ); + + await db.query("DELETE FROM documents WHERE hospital_id = ?", [id]); + + await db.query( + "DELETE FROM hospital_users WHERE hospital_code = ?", + [hospitalResult[0].hospital_code] + ); + + await db.query( + "DELETE FROM app_users WHERE hospital_code = ?", + [hospitalResult[0].hospital_code] + ); + + await db.query( + "DELETE FROM interaction_logs WHERE hospital_code = ?", + [hospitalResult[0].hospital_code] + ); + + const deleteQuery = "DELETE FROM hospitals WHERE id = ?"; + const result = await db.query(deleteQuery, [id]); + + if (result.affectedRows === 0) { + throw new Error("hospital not found"); + } + + return { message: "Hospital deleted successfully!" }; + } + + async getAllHospitalUsers() { + const hospitalUsers = await db.query(` + SELECT + u.id, + u.hospital_id, + u.email, + u.role_id, + r.name AS role_name, + u.status, + u.created_at, + u.updated_at + FROM + hospital_users u + JOIN + roles r + ON + u.role_id = r.id + `); + + return { + message: "Hospital users fetched successfully!", + data: hospitalUsers, + }; + } + + async getColorsFromHospital(userId, userRole) { + if (!["Superadmin", 7].includes(userRole)) { + throw new Error("You are not authorized to access hospital's colors!!"); + } + + const queryhspt_users = `SELECT hospital_id FROM hospital_users WHERE id = ?`; + const resulthspt_users = await db.query(queryhspt_users, [userId]); + + const queryhspt = `SELECT primary_color, secondary_color FROM hospitals WHERE id = ?`; + const resulthspt = await db.query(queryhspt, [resulthspt_users[0].hospital_id]); + + if (resulthspt.length === 0) { + throw new Error("Hospital not found"); + } + + return { + message: "Hospital colors fetched successfully!", + data: resulthspt, + }; + } + + async changePassword(id, new_password, token) { + if (!new_password) { + throw new Error("New password is required"); + } + + if (!token || !token.startsWith("Bearer ")) { + throw new Error("Authorization token is required"); + } + + const accessToken = token.split(" ")[1]; + let decodedToken; + try { + decodedToken = jwt.verify(accessToken, process.env.JWT_ACCESS_TOKEN_SECRET); + } catch (err) { + throw new Error("Invalid or expired token"); + } + + if (parseInt(id, 10) !== decodedToken.id) { + throw new Error("Token user does not match the requested user"); + } + + const numericId = parseInt(id, 10); + if (isNaN(numericId)) { + throw new Error("Invalid user ID"); + } + + const userQuery = `SELECT id FROM app_users WHERE id = ?`; + const [userResult] = await db.query(userQuery, [numericId]); + + if (!userResult || userResult.length === 0) { + throw new Error("User not found"); + } + + const hashedNewPassword = await bcrypt.hash(new_password, 10); + + const updatePasswordQuery = `UPDATE app_users SET hash_password = ? WHERE id = ?`; + await db.query(updatePasswordQuery, [hashedNewPassword, numericId]); + + return { message: "Password updated successfully!" }; + } + + generateRandomPassword(length = 12) { + return crypto + .randomBytes(Math.ceil(length / 2)) + .toString("hex") + .slice(0, length); + } + + async sendTempPassword(email) { + if (!email) { + throw new Error("Email is required"); + } + + const user = await db.query( + "SELECT id, primary_admin_email, name_hospital, admin_name FROM hospitals WHERE primary_admin_email = ?", + [email] + ); + if (!user.length) { + throw new Error("User not found"); + } + + const hsptuser = await db.query( + "SELECT id, email FROM hospital_users WHERE email = ?", + [email] + ); + if (!hsptuser.length) { + throw new Error("User not found"); + } + + const hsptId = user[0].id; + const hsptUsrId = hsptuser[0].id; + + const randomPassword = this.generateRandomPassword(); + const hashedPassword = await bcrypt.hash(randomPassword, 10); + const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000); + const type = "temp"; + + await db.query( + "UPDATE hospitals SET temporary_password = ?, expires_at = ?, type = ? WHERE id = ?", + [randomPassword, expiresAt, type, hsptId] + ); + + await db.query( + "UPDATE hospital_users SET temporary_password = ?, expires_at = ?, type = ? WHERE id = ?", + [randomPassword, expiresAt, type, hsptUsrId] + ); + + const info = await this.sendMail( + email, + user[0].name_hospital, + user[0].admin_name, + randomPassword + ); + + return { + message: "temporary password generated successfully", + email_status: info.response + }; + } + + async changeTempPassword(email, temp_password, new_password) { + if (!email || !temp_password || !new_password) { + throw new Error("Email, Temporary password, and new password are required"); + } + + const user = await db.query( + "SELECT id, temporary_password, expires_at, type FROM hospitals WHERE primary_admin_email = ?", + [email] + ); + if (!user.length) { + throw new Error("User not found"); + } + + const hsptuser = await db.query( + "SELECT id, temporary_password, expires_at, type FROM hospital_users WHERE email = ?", + [email] + ); + if (!hsptuser.length) { + throw new Error("User not found"); + } + + const isMatch = temp_password === user[0].temporary_password; + + if (!isMatch) { + throw new Error("Invalid temporary password"); + } + + if (new Date() > new Date(user[0].expires_at)) { + throw new Error("temporary password expired. Request a new one."); + } + + const hashedPassword = await bcrypt.hash(new_password, 10); + + await db.query( + "UPDATE hospitals SET primary_admin_password = ?, expires_at = ? ,type = NULL, temporary_password = NULL WHERE id = ?", + [hashedPassword, new Date(Date.now()), user[0].id] + ); + + await db.query( + "UPDATE hospital_users SET hash_password = ?, expires_at = ? ,type = NULL, temporary_password = NULL WHERE id = ?", + [hashedPassword, new Date(Date.now()), hsptuser[0].id] + ); + + return { message: "Password changed successfully!" }; + } + + async sendMail(email, hospital_name, adminName, randomPassword) { + const htmlContent = generatePasswordResetEmail(hospital_name, adminName, randomPassword); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: email, + subject: "Spurrinai temporary password", + html: htmlContent + }; + + try { + const info = await transporter.sendMail(mailOptions); + return info; + } catch (error) { + console.error(`Error sending email to ${email}:`, error); + throw error; + } + } + + async updateHospitalName(hospital_user_id, hospital_name) { + const queryuser = 'SELECT hospital_id FROM hospital_users WHERE id = ?'; + const [rows] = await db.execute(queryuser, [hospital_user_id]); + + const query = "UPDATE hospital_name SET name = ? WHERE id = ?"; + const values = [hospital_name, rows[0]?.hospital_id]; + + await db.query(query, values); + + return { message: "Name changed successfully!" }; + } + + async sendTemporaryPassword(email) { + if (!email) { + throw new Error("Email is required"); + } + + const hsptuser = await db.query( + "SELECT id, hospital_id, hash_password, name, expires_at, type FROM hospital_users WHERE email = ? AND role_id IN (8, 9)", + [email] + ); + + if (!hsptuser.length) { + throw new Error("User not found"); + } + + const hsptUsrId = hsptuser[0].id; + const hsptId = hsptuser[0].hospital_id; + const randomPassword = this.generateRandomPassword(); + const hashedPassword = await bcrypt.hash(randomPassword, 10); + const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000); + const type = "temp"; + + await db.query( + "UPDATE hospital_users SET hash_password = ?, expires_at = ?, type = ? WHERE id = ?", + [hashedPassword, expiresAt, type, hsptUsrId] + ); + + const hspt = await db.query( + "SELECT name_hospital FROM hospitals WHERE id = ?", + [hsptId] + ); + + const info = await this.sendMail( + email, + hspt[0].name_hospital, + hsptuser[0].name, + randomPassword + ); + + return { + message: "temporary password gerated successfully", + email_status: info.response + }; + } + + async changeTempPasswordAdminsViewers(email, temp_password, new_password) { + if (!email || !temp_password || !new_password) { + throw new Error("Email, Temporary password, and new password are required"); + } + + const hsptuser = await db.query( + "SELECT id, temporary_password, expires_at, type FROM hospital_users WHERE email = ? AND role_id IN (8, 9)", + [email] + ); + + if (!hsptuser.length) { + throw new Error("Email not found"); + } + + const isMatch = await bcrypt.compare( + temp_password, + hsptuser[0].temporary_password + ); + + if (!isMatch) { + throw new Error("Invalid temporary password"); + } + + if (new Date() > new Date(hsptuser[0].expires_at)) { + throw new Error("temporary password expired. Request a new one."); + } + + const hashedPassword = await bcrypt.hash(new_password, 10); + + await db.query( + "UPDATE hospital_users SET hash_password = ?, expires_at = ? ,type = NULL, temporary_password = NULL WHERE id = ?", + [hashedPassword, new Date(Date.now()), hsptuser[0].id] + ); + + return { message: "Password changed successfully!" }; + } + + async checkNewAppUser(hospital_code) { + if (!hospital_code) { + throw new Error("hospital code is required"); + } + + const appUser = await db.query( + "SELECT * FROM app_users WHERE hospital_code = ? AND checked = 0", + [hospital_code] + ); + + if (!appUser.length) { + throw new Error("No new user found"); + } + + return { message: "new notification found", appUser }; + } + + async updateAppUserChecked(id) { + if (!id) { + throw new Error("User ID is required"); + } + + const result = await db.query("UPDATE app_users SET checked = 1 WHERE id = ?", [id]); + + if (result.affectedRows === 0) { + throw new Error("User not found or already checked"); + } + + return { message: "User checked status updated successfully", updatedUserId: id }; + } + + async interactionLogs(hospital_code, app_user_id) { + if (!hospital_code && !app_user_id) { + throw new Error("hospital code or app user id is required"); + } + + let baseQuery = ` + SELECT il.*, au.email, au.username + FROM interaction_logs il + LEFT JOIN app_users au ON il.app_user_id = au.id + WHERE 1=1 + `; + const params = []; + + if (hospital_code) { + baseQuery += ` AND il.hospital_code = ?`; + params.push(hospital_code); + } + + if (app_user_id) { + baseQuery += ` AND il.app_user_id = ?`; + params.push(app_user_id); + } + + const intLogs = await db.query(baseQuery, params); + + if (!intLogs.length) { + throw new Error("No logs found"); + } + + return { message: "log data found", intLogs }; + } + + async updatePublicSignup(id, enabled, userId, userRole) { + if (typeof enabled !== 'boolean') { + throw new Error("Invalid input. 'enabled' must be a boolean value"); + } + + if (!["Spurrinadmin", "Superadmin", 7, 6].includes(userRole)) { + throw new Error("You are not authorized to update public signup settings"); + } + + if (userRole === "Superadmin") { + const hospital = await db.query( + "SELECT id FROM hospitals WHERE id = ?", + [id] + ); + + if (!hospital.length) { + throw new Error("hospital not found"); + } + + if (id != userId) { + throw new Error("You can only update public signup settings for your own hospital"); + } + } + + const result = await db.query( + 'UPDATE hospitals SET publicSignupEnabled = ? WHERE id = ?', + [enabled, id] + ); + + if (result.affectedRows === 0) { + throw new Error("Hospital not found or no changes made"); + } + + return { + status: 'success', + message: 'Hospital signup settings updated successfully.', + data: { + id, + publicSignupEnabled: enabled + } + }; + } + + async getPublicSignup(id, userId, userRole) { + if (!["Spurrinadmin", "Superadmin", 7, 6].includes(userRole)) { + throw new Error("You are not authorized to update public signup settings"); + } + + if (userRole === "Superadmin") { + const hospital = await db.query( + "SELECT id FROM hospitals WHERE id = ?", + [id] + ); + + if (!hospital.length) { + throw new Error("hospital not found"); + } + + if (id != userId) { + throw new Error("You can only get public signup settings for your own hospital"); + } + } + + const result = await db.query( + 'SELECT publicSignupEnabled from hospitals WHERE id = ?', + [id] + ); + + if (result.length === 0) { + return { + status: 'Not found', + message: 'Hospital not found or no changes made.', + }; + } + + return { + message: 'data fetched successfully.', + result + }; + } +} + +module.exports = new HospitalService(); diff --git a/src/services/nlpqamapper.js b/src/services/nlpqamapper.js new file mode 100644 index 0000000..42ccb71 --- /dev/null +++ b/src/services/nlpqamapper.js @@ -0,0 +1,299 @@ +const natural = require("natural"); +const TfIdf = natural.TfIdf; + +function withTimeout(promise, timeoutMs = 5000) { + let timeoutId; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`Operation timed out after ${timeoutMs}ms`)); + }, timeoutMs); + }); + + return Promise.race([ + promise, + timeoutPromise + ]).finally(() => { + clearTimeout(timeoutId); + }); +} + +const userSessionMap = new Map(); + +async function getAnswerFromQuestion(question, hospitalCode, userState = {}, context = []) { + try { + // Log question received + console.log('[NLQ] Received question:', question, '| Hospital:', hospitalCode, '| userState:', userState, '| context param:', context); + if (!question || typeof question !== 'string') { + return "Please provide a valid question."; + } + + if (!hospitalCode) { + return "Hospital information is required."; + } + + if (!userState) { + userState = { + awaitingConfirmation: false, + lastOriginalQuery: '' + }; + } + + // Require a unique session or user ID for every chat + let sessionId = userState.activeSessionId || userState.userId; + if (!sessionId && (userState.userSate || userState.userSateId)) { + sessionId = userState.userSate || userState.userSateId; + console.warn('[NLQ] WARNING: Using non-standard session key (userSate or userSateId). Please update frontend to use userId or activeSessionId.'); + } + if (!sessionId) { + console.error('[NLQ] No session or user ID provided. Rejecting request to prevent data leakage.'); + return "A unique session or user identifier is required for this chat. Please refresh or log in again."; + } + const sessionKey = `${hospitalCode}-${sessionId}`; + console.log(`[NLQ] Using sessionKey: ${sessionKey} (sessionId: ${sessionId})`); + // Context is managed per sessionKey. Only the last 5 questions are kept per session. + if (!userSessionMap.has(sessionKey)) { + userSessionMap.set(sessionKey, { + awaitingConfirmation: false, + lastOriginalQuery: '', + context: [] + }); + console.log(`[NLQ] Created new session for key: ${sessionKey}`); + } + const session = userSessionMap.get(sessionKey); + console.log(`[NLQ] Session context for key ${sessionKey}:`, session.context); + // Add the current question to the session context and keep only the last 5 questions + if (question && (!session.context.length || session.context[session.context.length - 1] !== question)) { + session.context.push(question); + if (session.context.length > 5) { + session.context = session.context.slice(-5); + } + console.log(`[NLQ] Updated session context for key: ${sessionKey}:`, session.context); + } + + if (session.awaitingConfirmation) { + const lowerQuestion = question.toLowerCase().trim(); + if (lowerQuestion === "yes" || lowerQuestion === "y") { + session.awaitingConfirmation = false; + console.log(`[NLQ] User confirmed general knowledge for session: ${sessionKey}`); + const answer = await handleGeneralKnowledgeResponse( + "yes", + session.lastOriginalQuery, + hospitalCode, + session.context, + userState + ); + + return answer || "I couldn't process that request. Please try again."; + } else if (lowerQuestion === "no" || lowerQuestion === "n") { + session.awaitingConfirmation = false; + console.log(`[NLQ] User denied general knowledge for session: ${sessionKey}`); + return "I'll stick to answering questions related to your hospital information."; + } + session.awaitingConfirmation = false; + } + + console.log(`[NLQ] [${sessionKey}] Flow 2: Attempting RAG approach with context:`, session.context); + try { + const ragAnswer = await withTimeout( + tryRAGApproach(question, hospitalCode, session.context, userState), + 120000 + ); + + if (ragAnswer) { + if ( + ragAnswer.includes("confirmation-prompt") && + !question.toLowerCase().includes("hospital") + ) { + session.awaitingConfirmation = true; + const originalQueryMatch = ragAnswer.match(/data-original-query="([^"]+)"/); + session.lastOriginalQuery = originalQueryMatch ? originalQueryMatch[1] : question; + console.log(`[NLQ] Waiting for user confirmation, original query:`, session.lastOriginalQuery); + } + console.log(`[NLQ] [${sessionKey}] RAG answer:`, ragAnswer); + return ragAnswer; + } + } catch (error) { + console.error(`[NLQ] [${sessionKey}] RAG approach failed with timeout or error:`, error.message); + } + + console.log(`[NLQ] [${sessionKey}] Flow 3: Attempting self-generate fallback with context:`, session.context); + try { + const fallbackAnswer = await withTimeout( + trySelfGenerateAnswer(question, hospitalCode, session.context, userState), + 15000 + ); + + if (fallbackAnswer) { + console.log(`[NLQ] [${sessionKey}] Self-generate answer:`, fallbackAnswer); + return fallbackAnswer; + } + } catch (error) { + console.error(`[NLQ] [${sessionKey}] Self-generate approach failed:`, error.message); + } + + console.log(`[NLQ] [${sessionKey}] No answer found, returning fallback message.`); + return "I don't have an answer for that question at the moment. Please try rephrasing or ask something else."; + } catch (error) { + console.error("Error in getAnswerFromQuestion:", error); + return "Sorry, I encountered an issue while processing your question. Please try again."; + } +} + +async function tryRAGApproach(question, hospitalCode, context, userState = {}) { + let retries = 2; + let lastError = null; + const session_id = userState.activeSessionId || userState.session_id; + console.log('[NLQ] [RAG] Sending to RAG API:', { question, hospitalCode, context }); + while (retries >= 0) { + try { + const response = await fetch(process.env.FLASK_BASE_URL + + "flask-api/generate-answer", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + question, + hospital_code: hospitalCode, + session_id, + context + }), + } + ); + + if (!response.ok) { + if (response.status === 429) { + console.log('[NLQ] [RAG] API rate limited (429)'); + return "Our system is currently handling many requests. Please try again in a moment."; + } + throw new Error(`RAG service returned status ${response.status}`); + } + + const data = await response.json(); + console.log('[NLQ] [RAG] Received from RAG API:', data); + + if (data.answer && data.answer.includes("confirmation-prompt")) { + console.log("[NLQ] [RAG] Received general knowledge confirmation prompt"); + return data.answer; + } + + if (!data.answer || + data.answer.trim() === "" || + data.answer.includes("I couldn't find an answer") || + data.answer.includes("Sorry")) { + return null; + } + + return data.answer; + } catch (error) { + lastError = error; + retries--; + if (retries >= 0) { + console.log(`[NLQ] [RAG] Retrying, ${retries} attempts left`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } + + console.error("[NLQ] [RAG] All retries failed:", lastError?.message); + return null; +} + +async function handleGeneralKnowledgeResponse(userResponse, originalQuery, hospitalCode, context, userState = {}) { + const session_id = userState.activeSessionId || userState.session_id; + console.log('[NLQ] [GeneralKnowledge] Sending confirmation:', { userResponse, originalQuery, hospitalCode, context }); + try { + const confirmResponse = await fetch( + process.env.FLASK_BASE_URL + "flask-api/generate-answer", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + question: userResponse, + hospital_code: hospitalCode, + is_response: true, + original_query: originalQuery, + session_id, + context + }), + } + ); + + if (!confirmResponse.ok) { + throw new Error(`General knowledge confirmation returned status ${confirmResponse.status}`); + } + + const confirmData = await confirmResponse.json(); + console.log('[NLQ] [GeneralKnowledge] Received confirmation response:', confirmData); + return confirmData.answer; + } catch (error) { + console.error("[NLQ] [GeneralKnowledge] Response failed:", error.message); + return "I couldn't process your request. Please try asking your question again."; + } +} + +async function trySelfGenerateAnswer(question, hospitalCode, context, userState = {}) { + const session_id = userState.activeSessionId || userState.session_id; + console.log('[NLQ] [SelfGenerate] Sending to self-generate API:', { question, hospitalCode, context }); + try { + const response = await fetch( + process.env.FLASK_BASE_URL + "flask-api/self-generate-answer", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + question, + hospital_code: hospitalCode, + session_id, + context + }), + } + ); + + if (!response.ok) { + throw new Error(`Self-generate service returned status ${response.status}`); + } + + const data = await response.json(); + console.log('[NLQ] [SelfGenerate] Received from self-generate API:', data); + return data.answer; + } catch (error) { + console.error("[NLQ] [SelfGenerate] Approach failed:", error.message); + return null; + } +} + +function getBestAnswer(newQuestion, userQuestions, userAnswers) { + const tfidf = new TfIdf(); + + if (!Array.isArray(userQuestions) || !Array.isArray(userAnswers) || + userQuestions.length === 0 || userAnswers.length === 0 || + userQuestions.length !== userAnswers.length) { + return "I don't have enough information to answer that."; + } + + userQuestions.forEach((question) => { + tfidf.addDocument(question); + }); + + tfidf.addDocument(newQuestion); + const newQuestionVector = tfidf.documents[tfidf.documents.length - 1]; + + const similarities = userQuestions.map((question, index) => { + const questionDoc = tfidf.documents[index]; + return natural.TfIdf.cosineSimilarity(newQuestionVector, questionDoc); + }); + + const bestIndex = similarities.indexOf(Math.max(...similarities)); + return userAnswers[bestIndex]; +} + +module.exports = { + getAnswerFromQuestion, + getBestAnswer, + handleGeneralKnowledgeResponse, +}; \ No newline at end of file diff --git a/src/services/onboardingService.js b/src/services/onboardingService.js new file mode 100644 index 0000000..5dd82c4 --- /dev/null +++ b/src/services/onboardingService.js @@ -0,0 +1,146 @@ +const db = require('../config/database'); + +class OnboardingService { + async getOnboardingSteps(userId, userRole, authenticatedUserId) { + // Validate the authenticated user is a Superadmin + if (userRole !== 'Superadmin') { + throw new Error('You are not authorized to fetch onboarding steps'); + } + + // Ensure the authenticated user's ID matches the userId from the URL + if (authenticatedUserId !== parseInt(userId, 10)) { + throw new Error('You are not authorized to fetch onboarding steps for this user'); + } + + if (!userId || isNaN(userId)) { + return { step: 'Pending' }; + } + + // Fetch onboarding steps for the authenticated user + const stepsQuery = 'SELECT * FROM onboarding_steps WHERE user_id = ?'; + const steps = await db.query(stepsQuery, [userId]); + + if (steps.length === 0) { + return { steps: 'Pending' }; + } + + return { message: 'Onboarding steps fetched successfully!', data: steps }; + } + + async addOnboardingStep(userId, step, userRole, authenticatedUserHospitalId) { + if (step === "Completed") { + const updateQuery = ` + UPDATE hospitals + SET onboarding_status = 'Completed' + WHERE id = ( + SELECT hospital_id FROM hospital_users WHERE id = ? + ); + `; + await db.query(updateQuery, [userId]); + } + + // Ensure the authenticated user is authorized to add the onboarding step + if (userRole !== 'Superadmin') { + throw new Error('You are not authorized to add onboarding steps'); + } + + // Validate if the userId exists and is valid + const userValidationQuery = ` + SELECT id, hospital_id + FROM hospital_users + WHERE id = ? + `; + const userValidationResult = await db.query(userValidationQuery, [userId]); + + if (!userValidationResult || userValidationResult.length === 0) { + throw new Error('User not found'); + } + + const user = userValidationResult[0]; + + // Ensure the user belongs to the same hospital, if applicable + if (authenticatedUserHospitalId && authenticatedUserHospitalId !== user.hospital_id) { + throw new Error('You are not authorized to add onboarding steps for this user'); + } + + // Check if onboarding step already exists for the user + const onboardingStepQuery = ` + SELECT id + FROM onboarding_steps + WHERE user_id = ? + `; + const onboardingStepResult = await db.query(onboardingStepQuery, [userId]); + + if (onboardingStepResult.length === 0) { + // If no onboarding step exists, insert a new one + const insertQuery = ` + INSERT INTO onboarding_steps (user_id, step) + VALUES (?, ?) + `; + const insertResult = await db.query(insertQuery, [userId, step || 'Pending']); + + if (insertResult.affectedRows === 0) { + throw new Error('Failed to add onboarding step'); + } + + return { + message: 'Onboarding step added successfully!', + data: { id: insertResult.insertId, userId, step } + }; + } else { + // If onboarding step exists, update the existing record + const updateQuery = ` + UPDATE onboarding_steps + SET step = ? + WHERE user_id = ? + `; + const updateResult = await db.query(updateQuery, [step, userId]); + + if (updateResult.affectedRows === 0) { + throw new Error('No changes made to the onboarding step'); + } + + return { message: 'Onboarding step updated successfully!' }; + } + } + + async updateOnboardingStep(userId, step, userRole, authenticatedUserId) { + // Validate that the authenticated user is a Superadmin + if (userRole !== 'Superadmin') { + throw new Error('You are not authorized to update onboarding steps'); + } + + // Ensure the authenticated user's ID matches the user_id from the URL + if (authenticatedUserId !== parseInt(userId, 10)) { + throw new Error('You are not authorized to update onboarding steps for this user'); + } + + // Validate that the onboarding step exists for the target user + const onboardingStepQuery = ` + SELECT id + FROM onboarding_steps + WHERE user_id = ? + `; + const onboardingStepResult = await db.query(onboardingStepQuery, [userId]); + + if (!onboardingStepResult || onboardingStepResult.length === 0) { + throw new Error('Onboarding step not found for the given user_id'); + } + + // Update the onboarding step for the target user + const updateQuery = ` + UPDATE onboarding_steps + SET step = ? + WHERE user_id = ? + `; + const updateResult = await db.query(updateQuery, [step, userId]); + + if (updateResult.affectedRows === 0) { + throw new Error('No changes made to the onboarding step'); + } + + return { message: 'Onboarding step updated successfully!' }; + } +} + +module.exports = new OnboardingService(); \ No newline at end of file diff --git a/src/services/roleService.js b/src/services/roleService.js new file mode 100644 index 0000000..8eb729f --- /dev/null +++ b/src/services/roleService.js @@ -0,0 +1,5 @@ +const db = require('../config/database'); + +exports.getAllRoles = async () => { + return await db.query('SELECT * FROM roles'); +}; diff --git a/src/services/secondaryWebsocket.js b/src/services/secondaryWebsocket.js new file mode 100644 index 0000000..af25485 --- /dev/null +++ b/src/services/secondaryWebsocket.js @@ -0,0 +1,366 @@ +const fs = require("fs"); +const https = require("https"); +const WebSocket = require("ws"); +const jwt = require("jsonwebtoken"); +const fetch = require("node-fetch"); +const db = require("../config/database"); + +const base_url = process.env.back_url; + + + +const server = https.createServer({ + // cert: fs.readFileSync(process.env.SSL_CERT), + // key: fs.readFileSync(process.env.SSL_KEY) +}); + + +const wss = new WebSocket.Server({ server, perMessageDeflate: false }); +const userSockets = new Map(); + +console.log("✅ Secure WebSocket Server running on wss://0.0.0.0:40520"); + +wss.on("connection", (ws) => { + console.log("🔌 New client connected to secondary WebSocket"); + + ws.on("message", async (message) => { + const data = JSON.parse(message); + + if (data.token && !ws.userId) { + try { + const decoded = jwt.verify(data.token, process.env.JWT_ACCESS_TOKEN_SECRET); + ws.userId = decoded.id; + userSockets.set(decoded.id, ws); + } catch { + ws.userId = null; + } + } + + if (data.event === "check-token-expiry") { + let decoded; + try { + decoded = jwt.verify(data.token, process.env.JWT_ACCESS_TOKEN_SECRET); + if (!decoded.exp) { + emitEvent("check-token-expiry", { expired: 1, message: 'Expiry not set to token' }, decoded.id); + return; + } + + const currentTime = Math.floor(Date.now() / 1000); + const timeLeft = decoded.exp - currentTime; + + if (timeLeft > 0) { + emitEvent("check-token-expiry", { expired: 0, message: `Token expires in ${Math.floor(timeLeft / 60)} minutes` }, decoded.id); + } else { + emitEvent("check-token-expiry", { expired: 1, message: "Token expired, please relogin" }, decoded.id); + } + } catch (error) { + emitEvent("check-token-expiry", { expired: 1, message: 'Token malformed', error }, ws.userId); + } + } + + if (data.event === "check-latest-token") { + if (!data.token) { + emitEvent("check-latest-token", { message: 'Access token required' }, ws.userId); + return; + } + + const decoded = jwt.decode(data.token); + if (!decoded) { + emitEvent("check-latest-token", { message: "Invalid token format" }, ws.userId); + return; + } + + ws.userId = decoded.id; + userSockets.set(decoded.id, ws); + + let table; + if (decoded.role === "Spurrinadmin") table = "super_admins"; + else if (["Admin", "Viewer", "Superadmin", 7, 8, 9].includes(decoded.role)) table = "hospital_users"; + else if (decoded.role === "AppUser") table = "app_users"; + else { + emitEvent("check-latest-token", { message: "Invalid role" }, decoded.id); + return; + } + + try { + const result = await db.query(`SELECT access_token FROM ${table} WHERE id = ?`, [decoded.id]); + const currentTime = Math.floor(Date.now() / 1000); + const timeLeft = decoded.exp - currentTime; + + if (result.length > 0 && result[0].access_token === data.token && timeLeft > 0) { + emitEvent("check-latest-token", { valid: 1, expired: 0, message: 'Token is valid' }, decoded.id); + } else { + emitEvent("check-latest-token", { valid: 0, expired: 0, message: 'Invalid token or expired' }, decoded.id); + } + } catch (error) { + emitEvent("check-latest-token", { valid: 0, expired: 0, message: "DB Error", error }, decoded.id); + } + } + + if (data.event === "check-notification") { + try { + const response = await fetch(base_url + "api/hospitals/check-user-notification", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${data.token}` + }, + body: JSON.stringify({ hospital_code: data.hospital_code }) + }); + + const result = await response.json(); + emitEvent("check-notification", { data: result, message: "New app users" }, ws.userId); + } catch (error) { + emitEvent("check-notification", { message: error.message }, ws.userId); + } + } + + if (data.event === "get-hospital-users") { + if (!data.token) { + emitEvent("get-hospital-users", { error: "Token missing" }, ws.userId); + return; + } + + try { + const decoded = jwt.verify(data.token, process.env.JWT_ACCESS_TOKEN_SECRET); + if (decoded.role !== 'Spurrinadmin' && decoded.role !== 6) { + emitEvent("get-hospital-users", { error: "Unauthorized access" }, ws.userId); + return; + } + + const users = await db.query("SELECT * FROM hospital_users"); + emitEvent("get-hospital-users", { data: users }, ws.userId); + } catch (error) { + emitEvent("get-hospital-users", { error: error.message }, ws.userId); + } + } + + if (data.event === "get-forwarded-feedbacks") { + if (!data.token) { + emitEvent("get-forwarded-feedbacks", { error: "Token missing" }, ws.userId); + return; + } + + try { + const decoded = jwt.verify(data.token, process.env.JWT_ACCESS_TOKEN_SECRET); + if (decoded.role !== 'Spurrinadmin' && decoded.role !== 6) { + emitEvent("get-forwarded-feedbacks", { error: "Unauthorized access" }, ws.userId); + return; + } + + const query = ` + SELECT + f.sender_type, + f.sender_id, + f.receiver_type, + f.receiver_id, + f.rating, + f.purpose, + f.information_received, + f.feedback_text, + f.created_at, + f.is_forwarded, + f.improvement, + h.name_hospital as sender_hospital, + h.hospital_code + FROM feedback f + LEFT JOIN hospitals h ON f.sender_id = h.id AND f.sender_type = 'hospital' + WHERE f.receiver_type = 'spurrin' + ORDER BY f.created_at DESC + `; + + const feedbacks = await db.query(query); + emitEvent("get-forwarded-feedbacks", { + message: "Forwarded feedbacks fetched successfully.", + data: feedbacks + }, ws.userId); + } catch (error) { + emitEvent("get-forwarded-feedbacks", { error: error.message }, ws.userId); + } + } + + // This event retrieves all feedback entries submitted by app users (sender_type = 'appuser') to a specific hospital (receiver_type = 'hospital') based on the hospital's hospital_code, which is derived from the JWT token provided by the user. + if (data.event === "get-app-user-byhospital-feedback") { + if (!data.token) { + emitEvent("get-app-user-byhospital-feedback", { error: "Token missing" }, ws.userId); + return; + } + + try { + const decoded = jwt.verify(data.token, process.env.JWT_ACCESS_TOKEN_SECRET); + + // Only hospital users (role 7, 8, or 9) are allowed + if (!["Superadmin","Admin",7, 8].includes(decoded.role)) { + emitEvent("get-app-user-byhospital-feedback", { error: "Unauthorized access" }, ws.userId); + return; + } + + + const email = decoded.email; + const userId = decoded.id; + + // Fetch hospital ID using the code + const hospitalCheck = await db.query( + "SELECT id FROM hospitals WHERE primary_admin_email = ?", + [email] + ); + + if (hospitalCheck.length === 0) { + emitEvent("get-app-user-byhospital-feedback", { error: "Hospital not found" }, userId); + return; + } + + const hospitalId = hospitalCheck[0].id; + + const query = ` + SELECT + f.feedback_id, + f.sender_type, + f.sender_id, + f.receiver_type, + f.receiver_id, + f.rating, + f.purpose, + f.information_received, + f.feedback_text, + f.improvement, + f.created_at, + f.is_forwarded, + au.username as user_name, + au.email as user_email + FROM feedback f + LEFT JOIN app_users au ON f.sender_id = au.id AND f.sender_type = 'appuser' + WHERE f.receiver_type = 'hospital' AND f.receiver_id = ? + ORDER BY f.created_at DESC + `; + + const feedbacks = await db.query(query, [hospitalId]); + + emitEvent("get-app-user-byhospital-feedback", { + message: "Hospital feedbacks fetched successfully.", + data: feedbacks + }, userId); + + } catch (error) { + emitEvent("get-app-user-byhospital-feedback", { error: error.message }, ws.userId); + } + } + + if (data.event === "get-documents-by-hospital") { + if (!data.token || !data.hospital_id) { + emitEvent("get-documents-by-hospital", { error: "Token or hospital_id missing" }, ws.userId); + return; + } + + try { + const decoded = jwt.verify(data.token, process.env.JWT_ACCESS_TOKEN_SECRET); + const allowedRoles = ['Admin', 'Superadmin', 'Viewer', 7, 8, 9]; + + // Role-based access check + if (!allowedRoles.includes(decoded.role)) { + emitEvent("get-documents-by-hospital", { error: "You are not authorized to view documents" }, decoded.id); + return; + } + + // Hospital access validation + const requestedHospitalId = parseInt(data.hospital_id, 10); + if (decoded.hospital_id !== requestedHospitalId) { + emitEvent("get-documents-by-hospital", { error: "Unauthorized hospital access" }, decoded.id); + return; + } + + // Fetch documents for hospital + const documents = await db.query( + "SELECT * FROM documents WHERE hospital_id = ?", + [requestedHospitalId] + ); + + emitEvent("get-documents-by-hospital", { + message: "Documents fetched successfully.", + documents + }, decoded.id); + + } catch (error) { + emitEvent("get-documents-by-hospital", { error: error.message }, ws.userId); + } + } + + if (data.event === "app-usersby-hospitalid") { + if (!data.token || !data.id) { + emitEvent("app-usersby-hospitalid", { error: "Token or hospital ID missing" }, ws.userId); + return; + } + + try { + const decoded = jwt.verify(data.token, process.env.JWT_ACCESS_TOKEN_SECRET); + const userRole = decoded.role; + + // Only allowed roles + if (!["Superadmin", "Admin", 8, 9].includes(userRole)) { + emitEvent("app-usersby-hospitalid", { error: "Unauthorized to view app users" }, decoded.id); + return; + } + + // Fetch hospital_code using hospital id + const query1 = `SELECT * FROM hospitals WHERE id = ?`; + const result1 = await db.query(query1, [data.id]); + + if (!result1 || !result1[0].hospital_code) { + emitEvent("app-usersby-hospitalid", { error: "Hospital not found" }, decoded.id); + return; + } + + console.log("result1:-------------------", result1); + + const hospitalCode = result1[0].hospital_code; + + // Fetch app users for that hospital_code + const query2 = `SELECT * FROM app_users WHERE hospital_code = ?`; + const users = await db.query(query2, [hospitalCode]); + + if (users.length === 0) { + emitEvent("app-usersby-hospitalid", { message: "No app users found" }, decoded.id); + return; + } + + emitEvent("app-usersby-hospitalid", { + message: "App users fetched successfully", + data: users + }, decoded.id); + + } catch (error) { + emitEvent("app-usersby-hospitalid", { error: error.message }, ws.userId); + } + } + + }); + + ws.on("close", () => { + console.log("❌ Client disconnected from secondary WebSocket"); + if (ws.userId && userSockets.has(ws.userId)) { + userSockets.delete(ws.userId); + } + ws.terminate(); + }); +}); + +function emitEvent(event, data, userId = null) { + if (userId && userSockets.has(userId)) { + const client = userSockets.get(userId); + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ event, data })); + } + } else { + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ event, data })); + } + }); + } +} + +server.listen(40520, () => { + console.log("📡 Secure WebSocket server listening on wss://backend.spurrinai.com:40520"); +}); + +module.exports = { wss, emitEvent }; diff --git a/src/services/superAdminService.js b/src/services/superAdminService.js new file mode 100644 index 0000000..4ad0a09 --- /dev/null +++ b/src/services/superAdminService.js @@ -0,0 +1,335 @@ +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const db = require('../config/database'); +const crypto = require("crypto"); +const transporter = require('../config/emailConfig'); +const generatePasswordResetEmail = require('../templates/passwordResetEmail'); + +class SuperAdminService { + async initializeSuperAdmin(email, password) { + if (!email || !password) { + throw new Error("Email and password are required"); + } + + const existingAdminQuery = 'SELECT id FROM super_admins WHERE email = ?'; + const existingAdminResult = await db.query(existingAdminQuery, [email]); + + if (existingAdminResult.length > 0) { + throw new Error('SuperAdmin with this email already exists'); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const insertQuery = ` + INSERT INTO super_admins (email, hash_password, role_id) + VALUES (?, ?, ?) + `; + const insertResult = await db.query(insertQuery, [email, hashedPassword, 6]); + + const superAdminId = insertResult.insertId; + + const payload = { + id: superAdminId, + email, + role: 'Spurrinadmin' + }; + const accessToken = jwt.sign(payload, process.env.JWT_ACCESS_TOKEN_SECRET, { expiresIn: process.env.JWT_ACCESS_TOKEN_EXPIRY }); + const refreshToken = jwt.sign(payload, process.env.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' }); + + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + const updateQuery = ` + UPDATE super_admins + SET refresh_token = ?, access_token = ?, access_token_expiry = ? + WHERE id = ? + `; + await db.query(updateQuery, [refreshToken, accessToken, expiryTimestamp, superAdminId]); + + return { + id: superAdminId, + email, + role: 'Spurrinadmin', + accessToken, + refreshToken + }; + } + + async getAllSuperAdmins(accessToken) { + if (!accessToken) { + throw new Error('Access token required'); + } + + const decoded = jwt.verify(accessToken, process.env.JWT_ACCESS_TOKEN_SECRET); + const { id, role } = decoded; + + if (role !== 'Spurrinadmin') { + throw new Error('Unauthorized role for this API'); + } + + const tokenValidationQuery = 'SELECT access_token, access_token_expiry FROM super_admins WHERE id = ?'; + const result = await db.query(tokenValidationQuery, [id]); + const superAdmin = result[0]; + + if (!superAdmin) { + throw new Error('Unauthorized access'); + } + + if (superAdmin.access_token !== accessToken) { + throw new Error('Invalid or expired access token'); + } + + const now = new Date(); + const expiryDate = new Date(superAdmin.access_token_expiry); + if (now > expiryDate) { + throw new Error('Access token has expired'); + } + + const fetchAdminsQuery = 'SELECT id, email, role_id FROM super_admins'; + return await db.query(fetchAdminsQuery); + } + + async addSuperAdmin(email, password) { + const checkAdminQuery = 'SELECT id FROM super_admins WHERE email = ?'; + const existingAdmin = await db.query(checkAdminQuery, [email]); + + if (existingAdmin.length > 0) { + throw new Error('SuperAdmin with this email already exists'); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const insertAdminQuery = ` + INSERT INTO super_admins (email, password, role_id) + VALUES (?, ?, ?) + `; + const result = await db.query(insertAdminQuery, [ + email, + hashedPassword, + 6 + ]); + + const newSuperAdminId = result.insertId; + + const payload = { id: newSuperAdminId, email, role: 'Spurrinadmin' }; + const accessToken = jwt.sign(payload, process.env.JWT_ACCESS_TOKEN_SECRET, { expiresIn: '5h' }); + const refreshToken = jwt.sign(payload, process.env.JWT_REFRESH_TOKEN_SECRET); + + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + const updateTokensQuery = ` + UPDATE super_admins + SET refresh_token = ?, access_token = ?, access_token_expiry = ? + WHERE id = ? + `; + await db.query(updateTokensQuery, [refreshToken, accessToken, expiryTimestamp, newSuperAdminId]); + + return { + id: newSuperAdminId, + email, + role: 'Spurrinadmin', + accessToken, + refreshToken + }; + } + + async deleteSuperAdmin(id) { + const result = await db.query('DELETE FROM super_admins WHERE id = ?', [id]); + if (result.affectedRows === 0) { + throw new Error('Super admin not found'); + } + } + + generateRandomPassword(length = 12) { + return crypto + .randomBytes(Math.ceil(length / 2)) + .toString("hex") + .slice(0, length); + } + + async sendTempPassword(email) { + if (!email) { + throw new Error("Email is required"); + } + + const user = await db.query( + "SELECT id, email FROM super_admins WHERE email = ?", + [email] + ); + + if (!user.length) { + throw new Error("User not found"); + } + + const superAdminId = user[0].id; + const randomPassword = this.generateRandomPassword(); + const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000); + const type = "temp"; + + await db.query( + "UPDATE super_admins SET temporary_password= ?, expires_at = ?, type = ? WHERE id = ?", + [randomPassword, expiresAt, type, superAdminId] + ); + + const info = await this.sendMail( + email, + 'Spurrin', + user[0].email, + randomPassword + ); + + return { + message: "temporary password generated successfully", + email_status: info.response + }; + } + + async changeTempPassword(email, temp_password, new_password) { + // Validate inputs + if (!email || !temp_password || !new_password) { + throw new Error("Email, Temporary password, and new password are required"); + } + + const user = await db.query( + "SELECT id, temporary_password, expires_at, type FROM super_admins WHERE email = ?", + [email] + ); + if (!user.length) { + throw new Error("User not found"); + } + + const isMatch = temp_password === user[0].temporary_password; + + // Check if temporary password matches + if (!isMatch) { + throw new Error("Invalid temporary password"); + } + + // Check if temporary password is expired + if (new Date() > new Date(user[0].expires_at)) { + throw new Error("temporary password expired. Request a new one."); + } + + // Hash the new password + const hashedPassword = await bcrypt.hash(new_password, 10); + + // Update password in DB & clear OTP + await db.query( + "UPDATE super_admins SET hash_password = ?, expires_at = ? ,type = NULL WHERE id = ?", + [hashedPassword, new Date(Date.now()), user[0].id] + ); + + return { message: "Password changed successfully!" }; + } + + async sendMail(email, hospital_name, adminName, randomPassword) { + const htmlContent = generatePasswordResetEmail(hospital_name, adminName, randomPassword); + + const mailOptions = { + from: process.env.EMAIL_USER, + to: email, + subject: "Spurrinai temporary password", + html: htmlContent, + }; + + try { + const info = await transporter.sendMail(mailOptions); + return info; + } catch (error) { + console.error(`Error sending email to ${email}:`, error); + throw error; + } + } + + async getDataConsumptionReport(userRole) { + if (userRole !== 'Spurrinadmin' && userRole !== 6) { + throw new Error("You are not authorized!"); + } + + // Overall metrics + const totalHospitalsQuery = 'SELECT COUNT(DISTINCT id) as total FROM hospitals'; + const totalHospitals = await db.query(totalHospitalsQuery); + + // Active hospitals + const activeHospitalsQuery = ` + SELECT COUNT(DISTINCT h.id) as active_count + FROM hospitals h + INNER JOIN users u ON h.id = u.hospital_id + INNER JOIN questions q ON u.id = q.user_id + `; + const activeHospitals = await db.query(activeHospitalsQuery); + + // Active users + const activeUsersQuery = ` + SELECT COUNT(DISTINCT u.id) as active_users + FROM app_users u + INNER JOIN questions q ON u.id = q.user_id + `; + const activeUsers = await db.query(activeUsersQuery); + + // Per hospital metrics + const hospitalMetricsQuery = ` + SELECT + h.id as hospital_id, + h.name as hospital_name, + COUNT(DISTINCT u.id) as total_registered_users, + COUNT(DISTINCT CASE WHEN q.id IS NOT NULL THEN u.id END) as active_users + FROM hospitals h + LEFT JOIN users u ON h.id = u.hospital_id + LEFT JOIN questions q ON u.id = q.user_id + GROUP BY h.id, h.name + `; + const hospitalMetrics = await db.query(hospitalMetricsQuery); + + return { + overall_metrics: { + total_hospitals: totalHospitals[0].total, + active_hospitals: activeHospitals[0].active_count, + active_users: activeUsers[0].active_users + }, + hospital_metrics: hospitalMetrics.map(hospital => ({ + hospital_id: hospital.hospital_id, + hospital_name: hospital.hospital_name, + total_registered_users: hospital.total_registered_users, + active_users: hospital.active_users + })) + }; + } + + async getOnboardedHospitals(userRole) { + if (userRole !== 'Spurrinadmin' && userRole !== 6) { + throw new Error("You are not authorized!"); + } + + // Query to get all hospitals with completed onboarding status + const query = ` + SELECT + h.*, + COUNT(DISTINCT au.id) as total_app_users, + COUNT(DISTINCT hu.id) as total_hospital_users + FROM hospitals h + LEFT JOIN app_users au ON h.hospital_code = au.hospital_code + LEFT JOIN hospital_users hu ON h.hospital_code = hu.hospital_code + WHERE h.onboarding_status = 'completed' + GROUP BY h.id + ORDER BY h.created_at DESC + `; + + const hospitals = await db.query(query); + + if (hospitals.length === 0) { + return { + message: "No onboarded hospitals found", + data: [] + }; + } + + return { + message: "Onboarded hospitals fetched successfully", + data: hospitals + }; + } +} + +module.exports = new SuperAdminService(); \ No newline at end of file diff --git a/src/services/tokenService.js b/src/services/tokenService.js new file mode 100644 index 0000000..8a89f4f --- /dev/null +++ b/src/services/tokenService.js @@ -0,0 +1,42 @@ +const jwt = require('jsonwebtoken'); + +exports.generateAccessToken = (user) => { + return jwt.sign(user, process.env.JWT_ACCESS_TOKEN_SECRET, { expiresIn: '5h' }); +}; + +exports.generateRefreshToken = (user) => { + return jwt.sign(user, process.env.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' }); +}; + +exports.verifyAccessToken = (token) => { + return jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); +}; + +exports.verifyRefreshToken = (token) => { + return jwt.verify(token, process.env.JWT_REFRESH_TOKEN_SECRET); +}; + + +exports.verifyToken = (token, secret) => { + return jwt.verify(token, secret); +}; + + + +// const jwt = require('jsonwebtoken'); + +// module.exports = { +// generateAccessToken: (payload) => { +// return jwt.sign(payload, process.env.JWT_ACCESS_TOKEN_SECRET, { +// expiresIn: process.env.JWT_ACCESS_TOKEN_EXPIRY || '15m', +// }); +// }, +// generateRefreshToken: (payload) => { +// return jwt.sign(payload, process.env.JWT_REFRESH_TOKEN_SECRET, { +// expiresIn: process.env.JWT_REFRESH_TOKEN_EXPIRY || '7d', +// }); +// }, +// verifyToken: (token, secret) => { +// return jwt.verify(token, secret); +// }, +// }; diff --git a/src/services/userService.js b/src/services/userService.js new file mode 100644 index 0000000..44b1fe8 --- /dev/null +++ b/src/services/userService.js @@ -0,0 +1,631 @@ +const bcrypt = require('bcrypt'); +const db = require('../config/database'); +const tokenService = require('./tokenService'); +const jwt = require('jsonwebtoken'); +const path = require('path'); +const fs = require('fs'); +const JWT_ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_TOKEN_SECRET; +const JWT_ACCESS_TOKEN_EXPIRY = process.env.JWT_ACCESS_TOKEN_EXPIRY || '5h'; +const JWT_REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_TOKEN_SECRET; +const generateWelcomeEmail = require('../templates/welcomeEmail'); +const transporter = require('../config/emailConfig'); +const back_url = process.env.BACK_URL; + +class UserService { + // Utility function to resolve the table name based on role_id + resolveTableName(roleId) { + if (roleId === 6) return 'super_admins'; // Spurrinadmin + if ([7, 8, 9].includes(roleId)) return 'hospital_users'; // Other roles + throw new Error('Invalid role_id'); + } + + async addUser(hospital_id, role_id, userData, requestorRole, requestorHospitalId) { + // Step 1: Validate the role of the requestor + if (!['Superadmin', 'Admin', 7, 8].includes(requestorRole)) { + throw new Error('Access denied. Only Superadmin and Admin can add users.'); + } + + if (![8, 9].includes(role_id)) { + throw new Error(`Access denied, cannot add user with role_id ${role_id}`); + } + + // Step 2: Validate the hospital_id + if (hospital_id !== requestorHospitalId) { + throw new Error('Access denied. You can only add users to your hospital.'); + } + + // Check email if already exists + const spurrinEmailQuery = "SELECT email from super_admins WHERE email = ?"; + const spurrinEmailResult = await db.query(spurrinEmailQuery, [userData.email]); + + if (spurrinEmailResult.length > 0) { + throw new Error("Email already exists!"); + } + + const hsptUsrEmailQuery = "SELECT email from hospital_users WHERE email = ?"; + const hsptUsrEmailResult = await db.query(hsptUsrEmailQuery, [userData.email]); + + if (hsptUsrEmailResult.length > 0) { + throw new Error("Email already exists!"); + } + + // Get hospital details + const hospitalQuery = 'SELECT * FROM hospitals WHERE id = ?'; + const hospitalResult = await db.query(hospitalQuery, [hospital_id]); + + if (!hospitalResult || hospitalResult.length === 0) { + throw new Error('Hospital not found for the given hospital_id'); + } + + const hospital_code = hospitalResult[0].hospital_code; + + const hospitalUsersQuery = 'SELECT * FROM hospital_users WHERE hospital_id = ?'; + const hospitalUserResult = await db.query(hospitalUsersQuery, [hospital_id]); + + if (!hospitalUserResult || hospitalUserResult.length === 0) { + throw new Error('Hospital not found for the given hospital_id'); + } + + const profile_photo_url = hospitalUserResult[0].profile_photo_url; + + // Step 3: Resolve table name based on role_id + const tableName = this.resolveTableName(role_id); + + // Step 4: Hash the password + const passwordHash = await bcrypt.hash(userData.password, 10); + + // Step 5: Insert user into the appropriate table + const query = ` + INSERT INTO ${tableName} + (hospital_code, hospital_id, email, hash_password, role_id, is_default_admin, requires_onboarding, + password_reset_required, profile_photo_url, phone_number, bio, status, name, department, + location, mobile_number, city) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + const result = await db.query(query, [ + hospital_code, + hospital_id, + userData.email, + passwordHash, + role_id, + userData.is_default_admin, + 0, + userData.password_reset_required, + profile_photo_url, + userData.phone_number, + userData.bio, + userData.status, + userData.name, + userData.department, + userData.location, + userData.mobile_number, + userData.city + ]); + + // Step 6: Generate tokens for the new user + const payload = { id: result.insertId, email: userData.email, role: role_id }; + const accessToken = tokenService.generateAccessToken(payload); + const refreshToken = tokenService.generateRefreshToken(payload); + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + // Step 7: Store the refresh token in the database + const updateQuery = `UPDATE ${tableName} SET refresh_token = ?, access_token = ?, access_token_expiry = ? WHERE id = ?`; + await db.query(updateQuery, [refreshToken, accessToken, expiryTimestamp, result.insertId]); + + // Send welcome email + const mailOptions = { + from: process.env.EMAIL_USER, + to: userData.email, + subject: 'Spurrinai Login Credentials', + html: generateWelcomeEmail({ + email: userData.email, + hospital_name: hospitalResult[0].name_hospital, + subdomain: hospitalResult[0].subdomain, + password: userData.password, + name: userData.name, + back_url: back_url, + }), + }; + + let emailInfo; + try { + const info = await transporter.sendMail(mailOptions); + emailInfo = info.response; + } catch (emailError) { + console.error("Email sending failed:", emailError.message); + emailInfo = "Email sending failed: " + emailError.message; + } + + return { + message: 'User added successfully!', + user: { id: result.insertId, role_id: role_id, ...userData }, + accessToken, + refreshToken, + emailInfo + }; + } + + async getUsersByHospital(hospital_id, userRole, userHospitalId) { + if (isNaN(hospital_id)) { + throw new Error('Invalid hospital ID'); + } + + // Ensure the authenticated user has access to the requested hospital + if ( + (userRole === 'Admin' && userHospitalId !== hospital_id) || + (userRole === 'Superadmin' && userHospitalId !== hospital_id) || + (userRole === 8 && userHospitalId !== hospital_id) || + (userRole === 9 && userHospitalId !== hospital_id) + ) { + throw new Error('You are not authorized to access this hospital'); + } + + const query = ` + SELECT * + FROM hospital_users + WHERE hospital_id = ? + `; + const users = await db.query(query, [hospital_id]); + + return { + message: 'Users fetched successfully', + users, + }; + } + + async getProfilePhoto(userId, userRole) { + if ( + (userRole === 'Admin') || + (userRole === 'Superadmin') || + (userRole === 8) + ) { + throw new Error('You are not authorized to access this hospital'); + } + + const query = 'SELECT profile_photo_url FROM hospital_users WHERE id = ?'; + const result = await db.query(query, [userId]); + + if (!result || result.length === 0) { + throw new Error('Profile photo not found'); + } + + return { + message: 'Profile photo fetched successfully!', + profile_photo_url: result[0].profile_photo_url, + }; + } + + async login(email, password) { + const user = await this.findUserByEmail(email); + if (!user) { + throw new Error('Invalid email or password'); + } + + const isValidPassword = await bcrypt.compare(password, user.hash_password); + if (!isValidPassword) { + throw new Error('Invalid email or password'); + } + + // Generate tokens + const payload = { id: user.id, email: user.email, role: user.role_id }; + const accessToken = tokenService.generateAccessToken(payload); + const refreshToken = tokenService.generateRefreshToken(payload); + + // Store the refresh token in the database + await this.updateRefreshToken(user.id, refreshToken); + + return { accessToken, refreshToken }; + } + + async logout(token) { + if (!token) { + throw new Error('Access token required'); + } + + const decoded = jwt.decode(token); + if (!['Spurrinadmin', 'Superadmin', 'Admin', 'Viewer', 6, 7, 8, 9].includes(decoded.role)) { + throw new Error('Unauthorized access'); + } + + + return { message: 'Logout successful!' }; + } + + async uploadProfilePhoto(userId, file) { + if (!file) { + throw new Error('No file uploaded'); + } + + const photoPath = `/uploads/profile_photos/${file.filename}`; + + // Update the photo URL in the database + await db.query( + 'UPDATE hospital_users SET profile_photo_url = ? WHERE id = ?', + [photoPath, userId] + ); + + return { + message: 'Profile photo uploaded successfully!', + profile_photo_url: photoPath, + }; + } + + async editHospitalUser(id, updatedData, requestorRole) { + if (!id) { + throw new Error('User ID is required'); + } + + // Step 1: Validate the role of the requestor + if (!['Superadmin', 'Admin', 'Viewer', 8, 9, 7].includes(requestorRole)) { + throw new Error('Access denied. Only Superadmin, user and Admin can update users.'); + } + + const allowedFields = [ + 'hospital_id', + 'email', + 'hash_password', + 'expires_at', + 'type', + 'role_id', + 'is_default_admin', + 'requires_onboarding', + 'password_reset_required', + 'profile_photo_url', + 'phone_number', + 'bio', + 'status', + 'name', + 'department', + 'location', + 'mobile_number', + 'access_token', + 'access_token_expiry', + 'hospital_code', + 'city' + ]; + + // Build dynamic SQL query + const fields = []; + const values = []; + for (const [key, value] of Object.entries(updatedData)) { + if (allowedFields.includes(key)) { + fields.push(`${key} = ?`); + values.push(value); + } + } + + if (fields.length === 0) { + throw new Error('No valid fields provided for update.'); + } + values.push(id); + + const query = `UPDATE hospital_users SET ${fields.join(', ')} WHERE id = ?`; + const result = await db.query(query, values); + + if (result.affectedRows === 0) { + throw new Error('Hospital user not found'); + } + + // Generate new tokens if role_id is updated + if (updatedData.role_id) { + const payload = { id: result.insertId, email: updatedData.email, role: updatedData.role_id }; + const accessToken = tokenService.generateAccessToken(payload); + const refreshToken = tokenService.generateRefreshToken(payload); + + const updateQuery = `UPDATE hospital_users SET refresh_token = ?, access_token = ? WHERE id = ?`; + await db.query(updateQuery, [refreshToken, accessToken, result.insertId]); + } + + return { message: 'Hospital user updated successfully' }; + } + + async deleteHospitalUser(id, requestorRole) { + if (!id) { + throw new Error('User ID is required'); + } + + const hspt_user = await db.query( + "SELECT role_id FROM hospital_users WHERE id= ?", + [id] + ); + + if (hspt_user.length === 0) { + throw new Error('Hospital user not found'); + } + + const userRole = hspt_user[0].role_id; + + if (userRole == 7 && requestorRole == 8) { + throw new Error('Access denied. You cannot delete super admin'); + } + + // Step 1: Validate the role of the requestor + if (!['Superadmin', 'Admin', 8, 7].includes(requestorRole)) { + throw new Error('Access denied. Only Superadmin and Admin can delete users.'); + } + + // Fetch all documents related to the hospital + const documents = await db.query( + "SELECT id, file_url FROM documents WHERE uploaded_by= ?", + [id] + ); + + // Delete document files dynamically + for (const document of documents) { + if (document.file_url) { + const filePath = path.join( + __dirname, + "..", + "uploads", + document.file_url.replace(/^\/uploads\//, "") + ); + + try { + await fs.promises.access(filePath, fs.constants.F_OK); + await fs.promises.unlink(filePath); + console.log("File deleted successfully:", filePath); + } catch (err) { + console.error(`Error deleting or accessing file ${filePath}: ${err.message}`); + } + } + } + + // Delete document-related records + await db.query( + "DELETE FROM questions_answers WHERE document_id IN (SELECT id FROM documents WHERE uploaded_by = ?)", + [id] + ); + + await db.query( + "DELETE FROM document_metadata WHERE document_id IN (SELECT id FROM documents WHERE uploaded_by = ?)", + [id] + ); + + await db.query( + "DELETE FROM document_pages WHERE document_id IN (SELECT id FROM documents WHERE uploaded_by = ?)", + [id] + ); + + // Delete the documents themselves + await db.query("DELETE FROM documents WHERE uploaded_by = ?", [id]); + + // Delete hospital user + await db.query("DELETE FROM hospital_users WHERE id = ?", [id]); + + return { message: "Hospital user deleted successfully" }; + } + + async getAccessToken(refreshToken, user_id) { + if (!refreshToken || !user_id) { + throw new Error('Refresh token and user ID are required'); + } + + // Verify the refresh token + const decoded = jwt.verify(refreshToken, JWT_REFRESH_TOKEN_SECRET); + const { email, role } = decoded; + + // Check if the refresh token exists in the database and belongs to the specified user + const query = ` + SELECT id, email, role_id, refresh_token + FROM hospital_users + WHERE id = ? AND refresh_token = ? + `; + const result = await db.query(query, [user_id, refreshToken]); + if (result.length === 0) { + throw new Error('Invalid or expired refresh token'); + } + + const user = result[0]; + + // Generate a new access token + const payload = { id: user.id, email: user.email, role }; + const newAccessToken = jwt.sign(payload, JWT_ACCESS_TOKEN_SECRET, { + expiresIn: JWT_ACCESS_TOKEN_EXPIRY, + }); + + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + const updateQuery = ` + UPDATE hospital_users + SET access_token = ?, access_token_expiry = ? + WHERE id = ? + `; + await db.query(updateQuery, [newAccessToken, expiryTimestamp, user_id]); + + return { + message: 'Access token generated and updated successfully', + accessToken: newAccessToken, + user_id: user.id, + }; + } + + async getAccessTokenForSpurrinadmin(refreshToken, user_id) { + if (!refreshToken || !user_id) { + throw new Error('Refresh token and user ID are required'); + } + + // Verify the refresh token + const decoded = jwt.verify(refreshToken, JWT_REFRESH_TOKEN_SECRET); + const { email, role } = decoded; + + // Check if the refresh token exists in the database and belongs to the specified user + const query = ` + SELECT id, email, role_id, refresh_token + FROM super_admins + WHERE id = ? AND refresh_token = ? + `; + const result = await db.query(query, [user_id, refreshToken]); + if (result.length === 0) { + throw new Error('Invalid or expired refresh token'); + } + + const user = result[0]; + + // Generate a new access token + const payload = { id: user.id, email: user.email, role }; + const newAccessToken = jwt.sign(payload, JWT_ACCESS_TOKEN_SECRET, { + expiresIn: JWT_ACCESS_TOKEN_EXPIRY, + }); + + const expiryTimestamp = new Date(); + expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); + + const updateQuery = ` + UPDATE super_admins + SET access_token = ?, access_token_expiry = ? + WHERE id = ? + `; + await db.query(updateQuery, [newAccessToken, expiryTimestamp, user_id]); + + return { + message: 'Access token generated and updated successfully', + accessToken: newAccessToken, + user_id: user.id, + }; + } + + async getRefreshTokenByUserId(user_id, role_id) { + let table; + let roleName; + + // Determine the correct table based on role_id + if (role_id == 6) { + table = 'super_admins'; + roleName = 'Spurrinadmin'; + } else if (role_id == 7 || role_id == 8 || role_id == 9) { + table = 'hospital_users'; + roleName = 'HospitalUser'; + } else { + throw new Error("Invalid role_id provided"); + } + + // Fetch refresh token from the selected table + const query = `SELECT refresh_token FROM ${table} WHERE id = ?`; + const result = await db.query(query, [user_id]); + + if (!result || result.length === 0) { + throw new Error('User not found or no refresh token available'); + } + + return { + message: 'Refresh token fetched successfully', + user_id: user_id, + role_id: role_id, + role_name: roleName, + refresh_token: result[0].refresh_token, + }; + } + + async getHospitalUserId(email, password) { + if (!email || !password) { + throw new Error('Email and password are required'); + } + + // Fetch user by email, including role_id and role name + const query = ` + SELECT sa.id, sa.hash_password, sa.role_id, r.name AS role_name + FROM super_admins sa + JOIN roles r ON sa.role_id = r.id + WHERE sa.email = ? + + UNION ALL + + SELECT hu.id, hu.hash_password, hu.role_id, r.name AS role_name + FROM hospital_users hu + JOIN roles r ON hu.role_id = r.id + WHERE hu.email = ? + `; + const result = await db.query(query, [email, email]); + + if (result.length === 0) { + throw new Error('User not found'); + } + + const user = result[0]; + + // Compare provided password with the stored hashed password + const isPasswordMatch = await bcrypt.compare(password, user.hash_password); + + if (!isPasswordMatch) { + throw new Error('Invalid email or password'); + } + + return { userId: user.id, roleId: user.role_id, roleName: user.role_name }; + } + + async updatePassword(id, new_password, token) { + if (!new_password) { + throw new Error('New password is required'); + } + + if (!token) { + throw new Error('Authorization token is required'); + } + + let decodedToken; + try { + decodedToken = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); + } catch (err) { + throw new Error('Invalid or expired token'); + } + + // Ensure the decoded token's user ID matches the route parameter + if (parseInt(id, 10) !== decodedToken.id) { + throw new Error('Token user does not match the requested user'); + } + + // Convert ID to integer and validate + const numericId = parseInt(id, 10); + if (isNaN(numericId)) { + throw new Error('Invalid user ID'); + } + + // Fetch the user from the database to ensure they exist + const userQuery = ` + SELECT id, hash_password FROM hospital_users WHERE id = ? + `; + const [userResult] = await db.query(userQuery, [numericId]); + + if (!userResult || userResult.length === 0) { + throw new Error('User not found'); + } + + const existingHashedPassword = userResult.hash_password; + const isSamePassword = await bcrypt.compare(new_password, existingHashedPassword); + + if (isSamePassword) { + throw new Error('New password must be different from the existing password'); + } + + // Hash the new password + const hashedNewPassword = await bcrypt.hash(new_password, 10); + + // Update the password in the database + const updatePasswordQuery = ` + UPDATE hospital_users SET hash_password = ? WHERE id = ? + `; + await db.query(updatePasswordQuery, [hashedNewPassword, numericId]); + + return { message: 'Password updated successfully!' }; + } + + // Helper methods + async findUserByEmail(email) { + const query = ` + SELECT * FROM hospital_users WHERE email = ? + UNION ALL + SELECT * FROM super_admins WHERE email = ? + `; + const result = await db.query(query, [email, email]); + return result[0]; + } + + async updateRefreshToken(userId, refreshToken) { + const query = 'UPDATE hospital_users SET refresh_token = ? WHERE id = ?'; + await db.query(query, [refreshToken, userId]); + } +} + +module.exports = new UserService(); diff --git a/src/services/webSocket.js b/src/services/webSocket.js new file mode 100644 index 0000000..fa4e3e8 --- /dev/null +++ b/src/services/webSocket.js @@ -0,0 +1,308 @@ +const jwt = require("jsonwebtoken"); +const db = require("../config/database"); // Database connection +const WebSocket = require("ws"); +const { getAnswerFromQuestion } = require("./nlpqamapper"); // Import the NLP processing module + +// Create WebSocket server +const wss = new WebSocket.Server({ port: 40510, perMessageDeflate: false }); + +// Map to store user connections - key: userId, value: connection object +const userConnections = new Map(); + +// Map to store user session states - key: userId, value: session data +const userSessionStates = new Map(); + +console.log("WebSocket Server running on ws://0.0.0.0:40510"); + +// Log active connections periodically to monitor server health +setInterval(() => { + console.log(`Active WebSocket connections: ${wss.clients.size}`); + console.log(`Mapped user connections: ${userConnections.size}`); +}, 60000); + +// Helper function to clean up when a connection is closed +function cleanupConnection(userId) { + if (userId && userConnections.has(userId)) { + console.log(`Cleaning up connection for user: ${userId}`); + userConnections.delete(userId); + userSessionStates.delete(userId); + } +} + +wss.on("connection", function (ws, req) { + const origin = req.headers.origin || "Unknown Origin"; + console.log(`New client connected from: ${origin}`); + + // Set a unique connection ID for logging + ws.connectionId = Date.now() + Math.random().toString(36).substring(2, 10); + console.log(`Assigned connection ID: ${ws.connectionId}`); + + // Set connection metadata + ws.isAuthenticated = false; + ws.userId = null; + + // Send welcome message + ws.send(JSON.stringify({ message: "Welcome to WebSocket Server!" })); + + ws.on("message", async function (message) { + try { + const connectionLog = `[Conn: ${ws.connectionId}]`; + console.log(`${connectionLog} Received message`); + + const parsedMessage = JSON.parse(message); + const { query, token, session_id, session_title } = parsedMessage; + + // Ensure session_id is present and unique per session + let user_session_id = session_id; + if (!user_session_id) { + // Generate a new session_id if missing (UUID v4) + user_session_id = require('crypto').randomUUID(); + console.warn(`${connectionLog} No session_id provided. Generated new session_id: ${user_session_id}`); + } + console.log(`${connectionLog} Using session_id: ${user_session_id}`); + + console.log(`${connectionLog} Processing query, timestamp: ${new Date().toISOString()}`); + + // Validate basic requirements + if (!token || !query) { + console.error(`${connectionLog} Missing token or query:`, { token: !!token, query: !!query }); + ws.send(JSON.stringify({ error: "Token and query are required fields" })); + return; + } + + // Validate token + let userData; + let user_hospital_code; + try { + // Verify JWT + userData = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); + + // Verify access token in database + const tokenQuery = `SELECT access_token, hospital_code FROM app_users WHERE id = ?`; + const result = await db.query(tokenQuery, [userData.id]); + + user_hospital_code = result[0].hospital_code + + if (!result.length || token !== result[0].access_token) { + console.error(`${connectionLog} Token mismatch for user: ${userData.id}`); + ws.send(JSON.stringify({ error: "Invalid or mismatched access token" })); + return; + } + + // Set authenticated state + ws.isAuthenticated = true; + ws.userId = userData.id; + + // Register this connection in the users map + userConnections.set(userData.id, ws); + console.log(`${connectionLog} Authenticated user: ${userData.id}`); + + } catch (err) { + console.error(`${connectionLog} Token verification failed:`, err.message); + ws.send(JSON.stringify({ error: "Invalid or expired token" })); + return; + } + + const userId = userData.id; + let userQuery + + // Validate user is active + const hsptQuery = `SELECT * FROM hospitals WHERE hospital_code = ?`; + const hospitalData = await db.query(hsptQuery, [user_hospital_code]); + + if(hospitalData[0].publicSignupEnabled){ + userQuery = ` + SELECT id, hospital_code, status + FROM app_users + WHERE id = ? + `; + } + else{ + // Validate user is active + userQuery = ` + SELECT id, hospital_code, status + FROM app_users + WHERE id = ? AND status = 'Active' + `; + } + + const userResult = await db.query(userQuery, [userId]); + + if (userResult.length === 0) { + console.error(`${connectionLog} Unauthorized or inactive user: ${userId}`); + ws.send(JSON.stringify({ error: "Unauthorized or inactive user" })); + return; + } + + const hospital_code = userResult[0].hospital_code; + console.log(`${connectionLog} User ${userId} is active and belongs to hospital: ${hospital_code}`); + + // Get or initialize user session state + if (!userSessionStates.has(userId)) { + userSessionStates.set(userId, { + awaitingConfirmation: false, + lastOriginalQuery: '', + activeSessionId: null + }); + } + + // Get the user-specific session state + const userState = userSessionStates.get(userId); + + const receivedQuestion = query.toString(); + console.log(`${connectionLog} Received question from user ${userId}: ${receivedQuestion}`); + + // Fetch last 5 Q&A pairs for this user and session_id + let context = []; + try { + const contextQuery = ` + SELECT query, response + FROM interaction_logs + WHERE app_user_id = ? AND session_id = ? + ORDER BY id DESC + LIMIT 5 + `; + const contextResult = await db.query(contextQuery, [userId, user_session_id]); + // Reverse to get chronological order + context = contextResult.reverse(); + console.log(`${connectionLog} Context for user ${userId}, session ${user_session_id}:`, context); + } catch (contextErr) { + console.error(`${connectionLog} Error fetching context Q&A:`, contextErr); + } + + // Update the user's state with the active session BEFORE calling NLP + userState.activeSessionId = user_session_id; + userState.session_id = user_session_id; // Add for clarity + + // Process the query through NLP - pass context + console.log(`${connectionLog} Python API called at: ${new Date().toISOString()}`); + let response; + try { + response = await getAnswerFromQuestion( + receivedQuestion, + hospital_code, + userState, + context // Pass context as argument + ); + function getErrorCode(response) { + const match = response.match(/Error code: (\d+)/); + return match ? match[1] : "Unknown Error"; + } + let responseStatus = getErrorCode(response); + console.log("response error code----", getErrorCode(response)); + console.log("response status----", responseStatus); + if (responseStatus == 400) { + response = "We couldn't understand that request. Please check and try again."; + } else if (responseStatus == 401) { + response = "Session expired. Please log in and try again."; + } else if (responseStatus == 403) { + response = "You don't have permission to access this feature."; + } else if (responseStatus == 404) { + response = "Requested resource not found."; + } else if (responseStatus == 429) { + response = "We're handling a lot right now. Please wait a moment and try again."; + } else if (responseStatus == 500) { + response = "Something went wrong on our end. We're on it!"; + } else if (responseStatus == 502 || responseStatus == 503 || responseStatus == 504) { + response = "Service is temporarily unavailable. Please try again shortly."; + } + } catch (error) { + console.error(`${connectionLog} Error getting answer from NLP:`, error); + response = "Sorry, there was an error processing your request."; + } + console.log(`${connectionLog} Answer sent back to user ${userId}: ${response}`); + console.log(`${connectionLog} Received answer from Python at: ${new Date().toISOString()}`); + + // Log the interaction + const logQuery = ` + INSERT INTO interaction_logs (session_id, session_title, hospital_code, query, response, app_user_id) + VALUES (?, ?, ?, ?, ?, ?) + `; + + const logResult = await db.query(logQuery, [ + user_session_id, + session_title || "Chat Session", + hospital_code, + query, + response, + userId + ]); + + const insertId = logResult.insertId; + + // Retrieve the full log entry + const selectQuery = ` + SELECT * FROM interaction_logs WHERE id = ? + `; + const result = await db.query(selectQuery, [insertId]); + + // Get the specific user connection and check if it's valid + const userConnection = userConnections.get(userId); + + if (userConnection && userConnection.readyState === WebSocket.OPEN) { + console.log(`${connectionLog} Sending answer to user ${userId} at: ${new Date().toISOString()}`); + userConnection.send(JSON.stringify({ + answer: result, + type: "chat", + sessionId: userState.activeSessionId + })); + } else { + console.log(`${connectionLog} User ${userId} connection is not open or no longer valid.`); + // Clean up the invalid connection + if (userConnections.has(userId)) { + userConnections.delete(userId); + } + } + } catch (error) { + console.error( + `[Conn: ${ws.connectionId}] Error handling WebSocket message:`, + error.message, + error.stack + ); + + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + error: "Internal server error. Please try again later.", + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }) + ); + } + } + }); + + ws.on("close", function () { + console.log(`Connection closed: ${ws.connectionId}`); + if (ws.userId) { + cleanupConnection(ws.userId); + } + }); + + ws.on("error", function (error) { + console.error(`WebSocket error on connection ${ws.connectionId}:`, error); + if (ws.userId) { + cleanupConnection(ws.userId); + } + }); +}); + + + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM received, closing WebSocket server...'); + wss.close(() => { + console.log('WebSocket server closed'); + process.exit(0); + }); +}); + +process.on('SIGINT', () => { + console.log('SIGINT received, closing WebSocket server...'); + wss.close(() => { + console.log('WebSocket server closed'); + process.exit(0); + }); +}); + +module.exports = wss; diff --git a/src/templates/passwordResetEmail.js b/src/templates/passwordResetEmail.js new file mode 100644 index 0000000..d36edc0 --- /dev/null +++ b/src/templates/passwordResetEmail.js @@ -0,0 +1,169 @@ +const generatePasswordResetEmail = (hospital_name, adminName, randomPassword) => { + return ` + + + + + Reset Your Password or pin - Spurrinai Medical Platform + + + + + + `; +}; + +module.exports = generatePasswordResetEmail; \ No newline at end of file diff --git a/src/templates/welcomeEmail.js b/src/templates/welcomeEmail.js new file mode 100644 index 0000000..cf40923 --- /dev/null +++ b/src/templates/welcomeEmail.js @@ -0,0 +1,87 @@ +const generateWelcomeEmail = (data) => { + const { email, hospitalName, subdomain, password, adminName, back_url } = data; + return ` + + + + + Welcome to Spurrinai + + + + + + + +
              + + + + + + + + +
              + +`; +}; + +module.exports = generateWelcomeEmail; \ No newline at end of file diff --git a/src/utils/asyncHandler.js b/src/utils/asyncHandler.js new file mode 100644 index 0000000..4e042fb --- /dev/null +++ b/src/utils/asyncHandler.js @@ -0,0 +1,10 @@ +/** + * Wraps an async function to handle errors consistently + * @param {Function} fn - The async function to wrap + * @returns {Function} Express middleware function + */ +const asyncHandler = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; + +module.exports = asyncHandler; \ No newline at end of file diff --git a/src/utils/encryption.js b/src/utils/encryption.js new file mode 100644 index 0000000..139afe7 --- /dev/null +++ b/src/utils/encryption.js @@ -0,0 +1,113 @@ +const crypto = require('crypto'); +const logger = require('./logger'); + +class Encryption { + constructor() { + this.algorithm = 'aes-256-gcm'; + this.key = Buffer.from(process.env.ENCRYPTION_KEY || '', 'hex'); + this.ivLength = 16; + this.saltLength = 64; + this.tagLength = 16; + } + + encrypt(text) { + try { + if (!text) return null; + + // Generate a random initialization vector + const iv = crypto.randomBytes(this.ivLength); + + // Generate a random salt + const salt = crypto.randomBytes(this.saltLength); + + // Create cipher + const cipher = crypto.createCipheriv(this.algorithm, this.key, iv); + + // Encrypt the text + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + // Get the auth tag + const tag = cipher.getAuthTag(); + + // Combine IV, salt, tag, and encrypted text + return Buffer.concat([ + iv, + salt, + tag, + Buffer.from(encrypted, 'hex') + ]).toString('base64'); + } catch (error) { + logger.error('Encryption error:', error); + throw new Error('Encryption failed'); + } + } + + decrypt(encryptedData) { + try { + if (!encryptedData) return null; + + // Convert from base64 + const buffer = Buffer.from(encryptedData, 'base64'); + + // Extract IV, salt, tag, and encrypted text + const iv = buffer.slice(0, this.ivLength); + const salt = buffer.slice(this.ivLength, this.ivLength + this.saltLength); + const tag = buffer.slice(this.ivLength + this.saltLength, this.ivLength + this.saltLength + this.tagLength); + const encrypted = buffer.slice(this.ivLength + this.saltLength + this.tagLength); + + // Create decipher + const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv); + decipher.setAuthTag(tag); + + // Decrypt the text + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } catch (error) { + logger.error('Decryption error:', error); + throw new Error('Decryption failed'); + } + } + + // Hash function for passwords + hashPassword(password) { + try { + const salt = crypto.randomBytes(16).toString('hex'); + const hash = crypto.pbkdf2Sync( + password, + salt, + 1000, + 64, + 'sha512' + ).toString('hex'); + + return `${salt}:${hash}`; + } catch (error) { + logger.error('Password hashing error:', error); + throw new Error('Password hashing failed'); + } + } + + // Verify password against hash + verifyPassword(password, hashedPassword) { + try { + const [salt, hash] = hashedPassword.split(':'); + const verifyHash = crypto.pbkdf2Sync( + password, + salt, + 1000, + 64, + 'sha512' + ).toString('hex'); + + return hash === verifyHash; + } catch (error) { + logger.error('Password verification error:', error); + throw new Error('Password verification failed'); + } + } +} + +module.exports = new Encryption(); \ No newline at end of file diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 0000000..39a4934 --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,54 @@ +class AppError extends Error { + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} + +class ValidationError extends AppError { + constructor(message) { + super(message, 400); + this.name = 'ValidationError'; + } +} + +class AuthenticationError extends AppError { + constructor(message) { + super(message, 401); + this.name = 'AuthenticationError'; + } +} + +class AuthorizationError extends AppError { + constructor(message) { + super(message, 403); + this.name = 'AuthorizationError'; + } +} + +class NotFoundError extends AppError { + constructor(message) { + super(message, 404); + this.name = 'NotFoundError'; + } +} + +class DatabaseError extends AppError { + constructor(message) { + super(message, 500); + this.name = 'DatabaseError'; + } +} + +module.exports = { + AppError, + ValidationError, + AuthenticationError, + AuthorizationError, + NotFoundError, + DatabaseError +}; \ No newline at end of file diff --git a/src/utils/fix_refresh_token.js b/src/utils/fix_refresh_token.js new file mode 100644 index 0000000..5844451 --- /dev/null +++ b/src/utils/fix_refresh_token.js @@ -0,0 +1,43 @@ +const jwt = require('jsonwebtoken'); +require('dotenv').config(); // Load environment variables + +// Replace these values with the correct user details from the database +const user_id = 63; // Change this to the actual user ID from MySQL +const email = "yasha.khandelwal@tech4biz.io"; // Change to actual email +const role_id = 7; // Change this to the correct role_id + +// Define role mapping +const roleMap = { + 6: 'Spurrinadmin', + 7: 'Superadmin', + 8: 'Admin', + 9: 'Viewer', +}; + +const role = roleMap[role_id] || 'UnknownRole'; + + +// Ensure JWT Secret Key is loaded correctly +const SECRET_KEY = process.env.JWT_REFRESH_TOKEN_SECRET; +if (!SECRET_KEY) { + console.error("❌ ERROR: JWT_REFRESH_TOKEN_SECRET is not set in your .env file."); + process.exit(1); +} + +// Generate new refresh token +const newRefreshToken = jwt.sign( + { id: user_id, email, role }, + SECRET_KEY, + { expiresIn: '7d' } // You can increase to '30d' if needed +); + +console.log("\n✅ NEW REFRESH TOKEN GENERATED:"); +console.log(newRefreshToken); + + +/********************************************************************* + * Company: Tech4biz Solutions + * Author: Tech4biz Solutions team backend + * Description: Replace old formatted tokens with new + * Copyright: Copyright © 2025Tech4Biz Solutions. + *********************************************************************/ diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..2d166f1 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,49 @@ +const winston = require('winston'); +const path = require('path'); + +// Define log format +const logFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.splat(), + winston.format.json() +); + +// Create the logger +const logger = winston.createLogger({ + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + format: logFormat, + defaultMeta: { service: 'spurrinai-backend' }, + transports: [ + // Write all logs with level 'error' and below to 'error.log' + new winston.transports.File({ + filename: path.join('logs', 'error.log'), + level: 'error', + maxsize: 5242880, // 5MB + maxFiles: 5, + }), + // Write all logs with level 'info' and below to 'combined.log' + new winston.transports.File({ + filename: path.join('logs', 'combined.log'), + maxsize: 5242880, // 5MB + maxFiles: 5, + }), + ], +}); + +// If we're not in production, log to the console as well +if (process.env.NODE_ENV !== 'production') { + logger.add(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ), + })); +} + +// Create a stream object for Morgan +logger.stream = { + write: (message) => logger.info(message.trim()), +}; + +module.exports = logger; \ No newline at end of file diff --git a/src/utils/monitoring.js b/src/utils/monitoring.js new file mode 100644 index 0000000..59b4d46 --- /dev/null +++ b/src/utils/monitoring.js @@ -0,0 +1,210 @@ +const os = require('os'); +const logger = require('./logger'); + +class Monitoring { + constructor() { + this.metrics = { + requests: { + total: 0, + success: 0, + failed: 0, + byEndpoint: new Map() + }, + responseTime: { + min: Infinity, + max: 0, + avg: 0, + total: 0, + count: 0 + }, + errors: new Map(), + memory: { + heapUsed: [], + heapTotal: [], + external: [], + rss: [] + }, + cpu: { + usage: [], + loadAvg: [] + } + }; + + // Start periodic monitoring + this.startMonitoring(); + } + + startMonitoring() { + // Monitor memory usage every 5 minutes + setInterval(() => this.collectMemoryMetrics(), 5 * 60 * 1000); + + // Monitor CPU usage every 5 minutes + setInterval(() => this.collectCPUMetrics(), 5 * 60 * 1000); + + // Log metrics every hour + setInterval(() => this.logMetrics(), 60 * 60 * 1000); + } + + collectMemoryMetrics() { + const memoryUsage = process.memoryUsage(); + + this.metrics.memory.heapUsed.push(memoryUsage.heapUsed); + this.metrics.memory.heapTotal.push(memoryUsage.heapTotal); + this.metrics.memory.external.push(memoryUsage.external); + this.metrics.memory.rss.push(memoryUsage.rss); + + // Keep only last 24 hours of data (288 points at 5-minute intervals) + if (this.metrics.memory.heapUsed.length > 288) { + this.metrics.memory.heapUsed.shift(); + this.metrics.memory.heapTotal.shift(); + this.metrics.memory.external.shift(); + this.metrics.memory.rss.shift(); + } + } + + collectCPUMetrics() { + const cpus = os.cpus(); + const loadAvg = os.loadavg(); + + let totalIdle = 0; + let totalTick = 0; + + cpus.forEach(cpu => { + for (const type in cpu.times) { + totalTick += cpu.times[type]; + } + totalIdle += cpu.times.idle; + }); + + const cpuUsage = 100 - (totalIdle / totalTick * 100); + + this.metrics.cpu.usage.push(cpuUsage); + this.metrics.cpu.loadAvg.push(loadAvg[0]); + + // Keep only last 24 hours of data + if (this.metrics.cpu.usage.length > 288) { + this.metrics.cpu.usage.shift(); + this.metrics.cpu.loadAvg.shift(); + } + } + + trackRequest(endpoint, method, statusCode, responseTime) { + // Update request counts + this.metrics.requests.total++; + if (statusCode >= 200 && statusCode < 400) { + this.metrics.requests.success++; + } else { + this.metrics.requests.failed++; + } + + // Track by endpoint + const endpointKey = `${method} ${endpoint}`; + if (!this.metrics.requests.byEndpoint.has(endpointKey)) { + this.metrics.requests.byEndpoint.set(endpointKey, { + total: 0, + success: 0, + failed: 0 + }); + } + const endpointMetrics = this.metrics.requests.byEndpoint.get(endpointKey); + endpointMetrics.total++; + if (statusCode >= 200 && statusCode < 400) { + endpointMetrics.success++; + } else { + endpointMetrics.failed++; + } + + // Update response time metrics + this.metrics.responseTime.min = Math.min(this.metrics.responseTime.min, responseTime); + this.metrics.responseTime.max = Math.max(this.metrics.responseTime.max, responseTime); + this.metrics.responseTime.total += responseTime; + this.metrics.responseTime.count++; + this.metrics.responseTime.avg = this.metrics.responseTime.total / this.metrics.responseTime.count; + } + + trackError(error, context) { + const errorKey = error.name || 'UnknownError'; + if (!this.metrics.errors.has(errorKey)) { + this.metrics.errors.set(errorKey, { + count: 0, + lastOccurrence: null, + contexts: new Set() + }); + } + + const errorMetrics = this.metrics.errors.get(errorKey); + errorMetrics.count++; + errorMetrics.lastOccurrence = new Date(); + if (context) { + errorMetrics.contexts.add(context); + } + } + + logMetrics() { + const metrics = { + timestamp: new Date().toISOString(), + requests: { + total: this.metrics.requests.total, + success: this.metrics.requests.success, + failed: this.metrics.requests.failed, + byEndpoint: Object.fromEntries(this.metrics.requests.byEndpoint) + }, + responseTime: { + min: this.metrics.responseTime.min, + max: this.metrics.responseTime.max, + avg: this.metrics.responseTime.avg + }, + errors: Object.fromEntries( + Array.from(this.metrics.errors.entries()).map(([key, value]) => [ + key, + { + count: value.count, + lastOccurrence: value.lastOccurrence, + contexts: Array.from(value.contexts) + } + ]) + ), + memory: { + heapUsed: this.metrics.memory.heapUsed[this.metrics.memory.heapUsed.length - 1], + heapTotal: this.metrics.memory.heapTotal[this.metrics.memory.heapTotal.length - 1], + external: this.metrics.memory.external[this.metrics.memory.external.length - 1], + rss: this.metrics.memory.rss[this.metrics.memory.rss.length - 1] + }, + cpu: { + usage: this.metrics.cpu.usage[this.metrics.cpu.usage.length - 1], + loadAvg: this.metrics.cpu.loadAvg[this.metrics.cpu.loadAvg.length - 1] + } + }; + + logger.info('Application metrics', metrics); + } + + getHealthStatus() { + const memoryUsage = process.memoryUsage(); + const heapUsedPercentage = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100; + + return { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + memory: { + heapUsed: memoryUsage.heapUsed, + heapTotal: memoryUsage.heapTotal, + heapUsedPercentage, + external: memoryUsage.external, + rss: memoryUsage.rss + }, + cpu: { + usage: os.loadavg()[0], + cores: os.cpus().length + }, + requests: { + total: this.metrics.requests.total, + success: this.metrics.requests.success, + failed: this.metrics.requests.failed + } + }; + } +} + +module.exports = new Monitoring(); \ No newline at end of file diff --git a/src/utils/responseHandler.js b/src/utils/responseHandler.js new file mode 100644 index 0000000..a7e4bc7 --- /dev/null +++ b/src/utils/responseHandler.js @@ -0,0 +1,34 @@ +/** + * Standard success response + * @param {Object} res - Express response object + * @param {number} statusCode - HTTP status code + * @param {string} message - Success message + * @param {Object} data - Response data + */ +const successResponse = (res, statusCode = 200, message = 'Success', data = null) => { + res.status(statusCode).json({ + success: true, + message, + data + }); +}; + +/** + * Standard error response + * @param {Object} res - Express response object + * @param {number} statusCode - HTTP status code + * @param {string} message - Error message + * @param {Object} errors - Additional error details + */ +const errorResponse = (res, statusCode = 500, message = 'Error', errors = null) => { + res.status(statusCode).json({ + success: false, + message, + errors + }); +}; + +module.exports = { + successResponse, + errorResponse +}; \ No newline at end of file diff --git a/src/utils/validator.js b/src/utils/validator.js new file mode 100644 index 0000000..b7b47f4 --- /dev/null +++ b/src/utils/validator.js @@ -0,0 +1,117 @@ +const { AppError } = require('../middlewares/errorHandler'); +const logger = require('./logger'); + +class Validator { + static validateEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + static validatePassword(password) { + // At least 8 characters, 1 uppercase, 1 lowercase, 1 number, 1 special character + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + return passwordRegex.test(password); + } + + static validatePhoneNumber(phone) { + const phoneRegex = /^\+?[\d\s-]{10,}$/; + return phoneRegex.test(phone); + } + + static validateRequiredFields(data, requiredFields) { + const missingFields = requiredFields.filter(field => !data[field]); + if (missingFields.length > 0) { + throw new AppError(`Missing required fields: ${missingFields.join(', ')}`, 400); + } + } + + static validateObjectId(id) { + const objectIdRegex = /^[0-9a-fA-F]{24}$/; + return objectIdRegex.test(id); + } + + static sanitizeInput(input) { + if (typeof input !== 'string') return input; + + // Remove any HTML tags + input = input.replace(/<[^>]*>/g, ''); + + // Remove any script tags + input = input.replace(/)<[^<]*)*<\/script>/gi, ''); + + // Remove any potentially dangerous characters + input = input.replace(/[<>]/g, ''); + + return input.trim(); + } + + static validatePaginationParams(page, limit) { + const parsedPage = parseInt(page); + const parsedLimit = parseInt(limit); + + if (isNaN(parsedPage) || parsedPage < 1) { + throw new AppError('Invalid page number', 400); + } + + if (isNaN(parsedLimit) || parsedLimit < 1 || parsedLimit > 100) { + throw new AppError('Invalid limit value. Must be between 1 and 100', 400); + } + + return { + page: parsedPage, + limit: parsedLimit + }; + } + + static validateDateRange(startDate, endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + throw new AppError('Invalid date format', 400); + } + + if (start > end) { + throw new AppError('Start date must be before end date', 400); + } + + return { start, end }; + } + + static validateFileUpload(file, options = {}) { + const { + maxSize = 5 * 1024 * 1024, // 5MB + allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'], + required = false + } = options; + + if (required && !file) { + throw new AppError('File is required', 400); + } + + if (file) { + if (file.size > maxSize) { + throw new AppError(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`, 400); + } + + if (!allowedTypes.includes(file.mimetype)) { + throw new AppError(`File type not allowed. Allowed types: ${allowedTypes.join(', ')}`, 400); + } + } + + return true; + } + + static validateQueryParams(params, allowedParams) { + const invalidParams = Object.keys(params).filter(param => !allowedParams.includes(param)); + + if (invalidParams.length > 0) { + logger.warn(`Invalid query parameters detected: ${invalidParams.join(', ')}`); + throw new AppError(`Invalid query parameters: ${invalidParams.join(', ')}`, 400); + } + + return true; + } +} + +module.exports = Validator; \ No newline at end of file diff --git a/src/validators/hospitalValidator.js b/src/validators/hospitalValidator.js new file mode 100644 index 0000000..2ef5bbb --- /dev/null +++ b/src/validators/hospitalValidator.js @@ -0,0 +1,31 @@ +const Joi = require('joi'); + +const createHospitalSchema = Joi.object({ + name_hospital: Joi.string().required().min(2).max(100), + // subdomain: Joi.string().required().min(2).max(50).pattern(/^[a-z0-9-]+$/), + primary_admin_email: Joi.string().required().email(), + primary_admin_password: Joi.string().required().min(8), + primary_color: Joi.string().required().pattern(/^#[0-9A-Fa-f]{6}$/), + secondary_color: Joi.string().required().pattern(/^#[0-9A-Fa-f]{6}$/), + // logo_url: Joi.string().uri().allow(''), + admin_name: Joi.string().required().min(2).max(100), + mobile_number: Joi.string().required().pattern(/^\+?[1-9]\d{1,14}$/), + location: Joi.string().required().min(2).max(200), + super_admin_id: Joi.number().required().integer().positive() +}); + +const updateHospitalSchema = Joi.object({ + name_hospital: Joi.string().min(2).max(100), + primary_admin_password: Joi.string().min(8), + primary_color: Joi.string().pattern(/^#[0-9A-Fa-f]{6}$/), + secondary_color: Joi.string().pattern(/^#[0-9A-Fa-f]{6}$/), + logo_url: Joi.string().uri().allow(''), + admin_name: Joi.string().min(2).max(100), + mobile_number: Joi.string().pattern(/^\+?[1-9]\d{1,14}$/), + location: Joi.string().min(2).max(200) +}).min(1); // At least one field must be provided for update + +module.exports = { + createHospitalSchema, + updateHospitalSchema +}; \ No newline at end of file diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 0000000..dca0f5c --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,17 @@ +# Integration Tests + +This directory contains integration tests for the application. Integration tests should: +- Test interactions between different parts of the application +- Test API endpoints +- Test database operations +- Test external service integrations + +## Structure +- `api/` - Tests for API endpoints +- `database/` - Tests for database operations +- `services/` - Tests for service integrations + +## Running Tests +```bash +npm run test:integration +``` \ No newline at end of file diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 0000000..7e762ab --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,18 @@ +# Unit Tests + +This directory contains unit tests for the application. Unit tests should: +- Test individual functions and methods in isolation +- Mock external dependencies +- Be fast and reliable +- Cover edge cases and error conditions + +## Structure +- `controllers/` - Tests for controller functions +- `services/` - Tests for service layer functions +- `utils/` - Tests for utility functions +- `models/` - Tests for database models + +## Running Tests +```bash +npm run test:unit +``` \ No newline at end of file