v1.0.0-rc
This commit is contained in:
commit
dc39677783
29
.env.example
Normal file
29
.env.example
Normal 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
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/node_modules
|
||||
/.env
|
||||
/hospital_data
|
||||
/logs
|
||||
/error.log
|
||||
/uploads
|
||||
/llm-uploads
|
||||
/certificates
|
||||
229
CHANGES.md
Normal file
229
CHANGES.md
Normal 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
146
Jenkinsfile
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
__pycache__/model_manager.cpython-312.pyc
Normal file
BIN
__pycache__/model_manager.cpython-312.pyc
Normal file
Binary file not shown.
104
docs/API.md
Normal file
104
docs/API.md
Normal 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
139
model_manager.py
Normal 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
6
nodemon.json
Normal 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
8309
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
package.json
Normal file
65
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
public/images/email-banner.png
Normal file
BIN
public/images/email-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
200
readme.md
Normal file
200
readme.md
Normal 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
32
requirements.txt
Normal 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
34
scripts/setup.js
Normal 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
184
src/app.js
Normal 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
88
src/config/database.js
Normal 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
17
src/config/emailConfig.js
Normal 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
54
src/config/env.js
Normal 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
79
src/config/index.js
Normal 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
308
src/config/initDatabase.js
Normal 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
BIN
src/controllers/.DS_Store
vendored
Normal file
Binary file not shown.
99
src/controllers/analysisController.js
Normal file
99
src/controllers/analysisController.js
Normal 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 });
|
||||
}
|
||||
};
|
||||
1020
src/controllers/appUserController.js
Normal file
1020
src/controllers/appUserController.js
Normal file
File diff suppressed because it is too large
Load Diff
99
src/controllers/authController.js
Normal file
99
src/controllers/authController.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
437
src/controllers/documentsController.js
Normal file
437
src/controllers/documentsController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
30
src/controllers/exceldataController.js
Normal file
30
src/controllers/exceldataController.js
Normal 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 });
|
||||
}
|
||||
};
|
||||
119
src/controllers/feedbacksController.js
Normal file
119
src/controllers/feedbacksController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
315
src/controllers/hospitalController.js
Normal file
315
src/controllers/hospitalController.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
59
src/controllers/onboardingController.js
Normal file
59
src/controllers/onboardingController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
10
src/controllers/roleController.js
Normal file
10
src/controllers/roleController.js
Normal 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 });
|
||||
}
|
||||
};
|
||||
148
src/controllers/superAdminController.js
Normal file
148
src/controllers/superAdminController.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
260
src/controllers/userController.js
Normal file
260
src/controllers/userController.js
Normal 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
|
||||
187
src/middlewares/authMiddleware.js
Normal file
187
src/middlewares/authMiddleware.js
Normal 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.
|
||||
*********************************************************************/
|
||||
73
src/middlewares/errorHandler.js
Normal file
73
src/middlewares/errorHandler.js
Normal 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
114
src/middlewares/security.js
Normal 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
|
||||
};
|
||||
52
src/middlewares/uploadsMiddleware.js
Normal file
52
src/middlewares/uploadsMiddleware.js
Normal 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;
|
||||
34
src/middlewares/validateRequest.js
Normal file
34
src/middlewares/validateRequest.js
Normal 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;
|
||||
46
src/migrations/createMigration.js
Normal file
46
src/migrations/createMigration.js
Normal 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();
|
||||
157
src/migrations/migrationRunner.js
Normal file
157
src/migrations/migrationRunner.js
Normal 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();
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
67
src/migrations/migrations/app_users_pin_otp_setup.js
Normal file
67
src/migrations/migrations/app_users_pin_otp_setup.js
Normal 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');
|
||||
}
|
||||
}
|
||||
};
|
||||
44
src/migrations/migrations/city_to_hospital_users.js
Normal file
44
src/migrations/migrations/city_to_hospital_users.js
Normal 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');
|
||||
}
|
||||
}
|
||||
};
|
||||
43
src/migrations/migrations/delete_app_user_keep_data.js
Normal file
43
src/migrations/migrations/delete_app_user_keep_data.js
Normal 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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
36
src/migrations/migrations/sessions.js
Normal file
36
src/migrations/migrations/sessions.js
Normal 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');
|
||||
}
|
||||
};
|
||||
43
src/migrations/migrations/super-admins-temporary-password.js
Normal file
43
src/migrations/migrations/super-admins-temporary-password.js
Normal 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');
|
||||
}
|
||||
}
|
||||
};
|
||||
35
src/migrations/migrations/user_sessions_delete.js
Normal file
35
src/migrations/migrations/user_sessions_delete.js
Normal 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');
|
||||
}
|
||||
};
|
||||
26
src/migrations/runMigrations.js
Normal file
26
src/migrations/runMigrations.js
Normal 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();
|
||||
37
src/migrations/template.js
Normal file
37
src/migrations/template.js
Normal 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
14
src/routes/analysis.js
Normal 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
163
src/routes/appUsers.js
Normal 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
16
src/routes/auth.js
Normal 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
66
src/routes/documents.js
Normal 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
13
src/routes/exceldata.js
Normal 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
25
src/routes/feedbacks.js
Normal 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
158
src/routes/hospitals.js
Normal 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
16
src/routes/onboarding.js
Normal 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
8
src/routes/roles.js
Normal 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
61
src/routes/superAdmins.js
Normal 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
78
src/routes/users.js
Normal 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
520
src/schema
Normal 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
|
||||
);
|
||||
302
src/services/analysisService.js
Normal file
302
src/services/analysisService.js
Normal 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();
|
||||
161
src/services/appUserService.js
Normal file
161
src/services/appUserService.js
Normal 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
220
src/services/authService.js
Normal 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
167
src/services/cronJobs.js
Normal 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 };
|
||||
|
||||
|
||||
125
src/services/exceldataService.js
Normal file
125
src/services/exceldataService.js
Normal 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();
|
||||
351
src/services/feedbacksService.js
Normal file
351
src/services/feedbacksService.js
Normal 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();
|
||||
841
src/services/hospitalService.js
Normal file
841
src/services/hospitalService.js
Normal 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
299
src/services/nlpqamapper.js
Normal 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,
|
||||
};
|
||||
146
src/services/onboardingService.js
Normal file
146
src/services/onboardingService.js
Normal 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();
|
||||
5
src/services/roleService.js
Normal file
5
src/services/roleService.js
Normal file
@ -0,0 +1,5 @@
|
||||
const db = require('../config/database');
|
||||
|
||||
exports.getAllRoles = async () => {
|
||||
return await db.query('SELECT * FROM roles');
|
||||
};
|
||||
366
src/services/secondaryWebsocket.js
Normal file
366
src/services/secondaryWebsocket.js
Normal 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 };
|
||||
335
src/services/superAdminService.js
Normal file
335
src/services/superAdminService.js
Normal 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();
|
||||
42
src/services/tokenService.js
Normal file
42
src/services/tokenService.js
Normal 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
631
src/services/userService.js
Normal 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
308
src/services/webSocket.js
Normal 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;
|
||||
169
src/templates/passwordResetEmail.js
Normal file
169
src/templates/passwordResetEmail.js
Normal 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;
|
||||
87
src/templates/welcomeEmail.js
Normal file
87
src/templates/welcomeEmail.js
Normal 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
10
src/utils/asyncHandler.js
Normal 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
113
src/utils/encryption.js
Normal 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
54
src/utils/errors.js
Normal 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
|
||||
};
|
||||
43
src/utils/fix_refresh_token.js
Normal file
43
src/utils/fix_refresh_token.js
Normal 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
49
src/utils/logger.js
Normal 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
210
src/utils/monitoring.js
Normal 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();
|
||||
34
src/utils/responseHandler.js
Normal file
34
src/utils/responseHandler.js
Normal 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
117
src/utils/validator.js
Normal 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;
|
||||
31
src/validators/hospitalValidator.js
Normal file
31
src/validators/hospitalValidator.js
Normal 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
|
||||
};
|
||||
17
tests/integration/README.md
Normal file
17
tests/integration/README.md
Normal 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
18
tests/unit/README.md
Normal 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
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user