v1.0.0-rc

This commit is contained in:
rohitgir-879 2025-06-12 00:19:44 +05:30
commit dc39677783
94 changed files with 22571 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

29
.env.example Normal file
View File

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

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/node_modules
/.env
/hospital_data
/logs
/error.log
/uploads
/llm-uploads
/certificates

229
CHANGES.md Normal file
View File

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

146
Jenkinsfile vendored Normal file
View File

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

Binary file not shown.

2269
chat.py Normal file

File diff suppressed because it is too large Load Diff

104
docs/API.md Normal file
View File

@ -0,0 +1,104 @@
# API Documentation
## Authentication
All API endpoints require authentication using JWT tokens.
### Headers
```
Authorization: Bearer <token>
```
## 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`

139
model_manager.py Normal file
View File

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

6
nodemon.json Normal file
View File

@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": "js,json",
"ignore": ["node_modules", "public", "uploads"],
"exec": "node src/app.js"
}

8309
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
package.json Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

200
readme.md Normal file
View File

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

32
requirements.txt Normal file
View File

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

34
scripts/setup.js Normal file
View File

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

184
src/app.js Normal file
View File

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

88
src/config/database.js Normal file
View File

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

17
src/config/emailConfig.js Normal file
View File

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

54
src/config/env.js Normal file
View File

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

79
src/config/index.js Normal file
View File

@ -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'];

308
src/config/initDatabase.js Normal file
View File

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

BIN
src/controllers/.DS_Store vendored Normal file

Binary file not shown.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*********************************************************************/

View File

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

114
src/middlewares/security.js Normal file
View File

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

View File

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

View File

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

View File

@ -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 <migration_name>');
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();

View File

@ -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=<migration_name>');
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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

14
src/routes/analysis.js Normal file
View File

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

163
src/routes/appUsers.js Normal file
View File

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

16
src/routes/auth.js Normal file
View File

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

66
src/routes/documents.js Normal file
View File

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

13
src/routes/exceldata.js Normal file
View File

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

25
src/routes/feedbacks.js Normal file
View File

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

158
src/routes/hospitals.js Normal file
View File

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

16
src/routes/onboarding.js Normal file
View File

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

8
src/routes/roles.js Normal file
View File

@ -0,0 +1,8 @@
const express = require('express');
const roleController = require('../controllers/roleController');
const router = express.Router();
router.get('/', roleController.getAllRoles);
module.exports = router;

61
src/routes/superAdmins.js Normal file
View File

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

78
src/routes/users.js Normal file
View File

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

520
src/schema Normal file
View File

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

View File

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

View File

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

220
src/services/authService.js Normal file
View File

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

167
src/services/cronJobs.js Normal file
View File

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

View File

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

View File

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

View File

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

299
src/services/nlpqamapper.js Normal file
View File

@ -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,
};

View File

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

View File

@ -0,0 +1,5 @@
const db = require('../config/database');
exports.getAllRoles = async () => {
return await db.query('SELECT * FROM roles');
};

View File

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

View File

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

View File

@ -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);
// },
// };

631
src/services/userService.js Normal file
View File

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

308
src/services/webSocket.js Normal file
View File

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

View File

@ -0,0 +1,169 @@
const generatePasswordResetEmail = (hospital_name, adminName, randomPassword) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Your Password or pin - Spurrinai Medical Platform</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Syne:wght@400..800&display=swap');
body {
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
background-color: #ebf3fa;
color: #333;
}
.email-container {
max-width: 600px;
margin: 20px auto;
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
}
.email-header {
background: linear-gradient(135deg, #2193b0, #6dd5ed);
padding: 30px;
text-align: center;
color: white;
}
.hospital-name {
display: inline-block;
background-color: rgba(255, 255, 255, 0.2);
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
margin-bottom: 10px;
}
.email-header h1 {
margin: 10px 0 0;
font-size: 26px;
font-weight: 500;
}
.email-content {
padding: 40px 30px;
}
.greeting {
font-size: 30px;
font-weight: 700;
margin-bottom: 20px;
color: #303030;
}
.greeting-text{
font-size: 18px;
font-weight: 400;
margin-bottom: 20px;
line-height: 1.7;
color: #303030;
}
.verification-code {
background-color: #f5f9fc;
border: 1px solid #e0e9f0;
border-radius: 8px;
padding: 20px;
margin: 25px 0;
text-align: center;
}
.code {
font-family: 'Courier New', monospace;
font-size: 32px;
letter-spacing: 6px;
color: #2193b0;
font-weight: bold;
padding: 10px 0;
}
.reset-button {
display: block;
background-color: #2193b0;
color: white;
text-decoration: none;
text-align: center;
padding: 15px 20px;
border-radius: 5px;
margin: 30px auto;
max-width: 250px;
font-weight: 500;
transition: background-color 0.3s;
}
.reset-button:hover {
background-color: #1a7b92;
}
.expiry-note {
background-color: #fff8e1;
border-left: 4px solid #ffc107;
padding: 12px 15px;
margin: 25px 0;
font-size: 14px;
color: #856404;
}
.security-note {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 14px;
color: #666;
}
.email-footer {
background-color: #f5f9fc;
padding: 20px;
text-align: center;
font-size: 13px;
color: #888;
border-top: 1px solid #e0e9f0;
}
.support-link {
color: #2193b0;
text-decoration: none;
}
.device-info {
margin-top: 20px;
background-color: #f5f9fc;
padding: 15px;
border-radius: 6px;
font-size: 14px;
}
.device-info p {
margin: 5px 0;
color: #666;
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<div class="hospital-name"> ${hospital_name}</div>
<h1>Reset Your Password</h1>
</div>
<div class="email-content">
<div class="greeting">Hello ${adminName},</div>
<p class="greeting-text" >We received a request to <strong>reset the password</strong> for your account on the <strong> Spurrinai healthcare platform</strong>. For your security reasons, please verify this action.</p>
<div class="verification-code">
<p>Your temporary password:</p>
<div class="code">${randomPassword}</div>
<p>use same password to generate new password</p>
</div>
<a href="#" class="reset-button">Copy Password</a>
<div class="expiry-note">
<strong>Note:</strong> This verification code will expire in 2 hours for security reasons.
</div>
<div class="security-note">
<p>If you did not request this password reset, please contact our IT security team immediately at <a href="mailto:info@spurrinai.com" class="support-link">info@spurrinai.com</a> or call our support line at +1 (800) 555-1234.</p>
</div>
</div>
<div class="email-footer">
<p>© 2025 Spurrinai - Healthcare Data Management Platform</p>
<p>This is an automated message. Please do not reply to this email.</p>
<p>Need help? Contact <a href="mailto:support@spurrinai.com" class="support-link">support@spurrinai.com</a></p>
</div>
</div>
</body>
</html>`;
};
module.exports = generatePasswordResetEmail;

View File

@ -0,0 +1,87 @@
const generateWelcomeEmail = (data) => {
const { email, hospitalName, subdomain, password, adminName, back_url } = data;
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Spurrinai</title>
<style>
@media only screen and (max-width: 600px) {
.container { width: 100% !important; padding-block: 60px; }
.header-image { height: 150px !important; background-color: #F2F2F7; }
.content { padding: 20px !important; }
.credentials-table { font-size: 14px !important; }
}
</style>
</head>
<body style="margin: 0; padding: 0; font-family: Inter, sans-serif; background-color: #f4f4f4;">
<table role="presentation" style="width: 100%; border-collapse: collapse; display: flex; justify-content: center; align-items: center; height: 100vh;">
<tr>
<td align="center" style="padding: 0;">
<table role="presentation" class="container" style="width: 680px; margin: 0 auto; background-color: #F2F2F7; box-shadow: 0 0 10px rgba(0,0,0,0.1); border-radius: 12px;">
<tr>
<td style="padding: 0;">
<table role="presentation" style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 20px 25px; background-color: #F2F2F7;">
<h1 style="margin: 0; font-size: 24px; color: #333333;">Spurrinai</h1>
</td>
</tr>
<tr style="padding: 0px 0px; display: grid; width: 93%; margin: auto;">
<td class="header-image" style="height: 200px; background-color: #b5e8e0; background-image: url(${back_url})">
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="content" style="padding: 20px 25px;">
<h2 style="margin: 0 0 20px; font-size: 28px; color: #333333;">Greetings, ${adminName},</h2>
<p style="margin: 0 0 20px; font-size: 16px; line-height: 1.5; color: #666666;">
Congratulations! Your hospital, <span style="color: #4F5A68; font-weight: 600;">${hospitalName}</span>, has been successfully onboarded to <span style="color: #4F5A68; font-weight: 600;">Spurrinai</span>. We are excited to have you on board and look forward to supporting your hospital's needs.
</p>
<p style="margin: 0 0 20px; font-size: 16px; line-height: 1.5; color: #4F5A68;">
<strong style="font-weight: 600; color: #4F5A68;">Please find your hospital's login credentials below:</strong>
</p>
<table role="presentation" class="credentials-table rounded-table" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tbody style="border: 1px solid #dddddd; border-radius: 8px; display: list-item; list-style-type: none; background-color: #fff;">
<tr style="display: flex; list-style: none;">
<td style="width: 100%; color: #1B1B1B; padding: 10px; background-color: #F1fffe; border: 1px solid #dddddd; font-weight: 600;">Hospital Name</td>
<td style="width: 100%; padding: 10px; color: #151515; font-weight: 300; border: 1px solid #dddddd;">${hospitalName}</td>
</tr>
<tr style="display: flex; list-style: none;">
<td style="width: 100%; color: #1B1B1B; padding: 10px; background-color: #F1fffe; border: 1px solid #dddddd; font-weight: 600;">Domain</td>
<td style="width: 100%; padding: 10px; color: #151515; font-weight: 300; border: 1px solid #dddddd;">${subdomain}</td>
</tr>
<tr style="display: flex; list-style: none;">
<td style="width: 100%; color: #1B1B1B; padding: 10px; background-color: #F1fffe; border: 1px solid #dddddd; font-weight: 600;">Username</td>
<td style="width: 100%; padding: 10px; color: #151515; font-weight: 300; border: 1px solid #dddddd;">${email}</td>
</tr>
<tr style="display: flex; list-style: none;">
<td style="width: 100%; color: #1B1B1B; padding: 10px; background-color: #F1fffe; border: 1px solid #dddddd; font-weight: 600;">Temporary Password</td>
<td style="width: 100%; padding: 10px; color: #151515; font-weight: 300; border: 1px solid #dddddd;">${password}</td>
</tr>
</tbody>
</table>
<p style="margin: 0 0 20px; font-size: 16px; line-height: 1.5; color: #525660;">
For security reasons, we recommend changing your password immediately after logging in.
</p>
<table role="presentation" style="width: 100%;">
<tr>
<td align="left">
<a href="https://${subdomain}.spurrinai.com" style="display: inline-block; padding: 12px 24px; background-color: #4F5A68; color: #fff; text-decoration: none; border-radius: 4px; font-weight: bold;">Log In and Change Password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
};
module.exports = generateWelcomeEmail;

10
src/utils/asyncHandler.js Normal file
View File

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

113
src/utils/encryption.js Normal file
View File

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

54
src/utils/errors.js Normal file
View File

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

View File

@ -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.
*********************************************************************/

49
src/utils/logger.js Normal file
View File

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

210
src/utils/monitoring.js Normal file
View File

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

View File

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

117
src/utils/validator.js Normal file
View File

@ -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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/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;

View File

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

View File

@ -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
```

18
tests/unit/README.md Normal file
View File

@ -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
```