forked from rohit/spurrin-backend
dev-backup
This commit is contained in:
commit
5c511a7548
43
.env.example
Normal file
43
.env.example
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_USER=root
|
||||
DB_PASSWORD=Admin@123
|
||||
DB_NAME=spurrindev
|
||||
|
||||
EMAIL_USER=spurrinai@gmail.com
|
||||
EMAIL_PASS=wblspqqpzpxfcjaq
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
|
||||
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/
|
||||
|
||||
# email
|
||||
|
||||
mail = srikanth.mallikarjuna@tech4biz.io
|
||||
apppassword = 0peCQnLEZVfZ
|
||||
|
||||
# mail = info@spurrin.com
|
||||
# apppassword=nbF314CF84Ja
|
||||
|
||||
# PORT
|
||||
PORT = 3000
|
||||
|
||||
# zoho mail config for development mode
|
||||
|
||||
SENDER_PORT = 587
|
||||
SENDER_SECURITY = false
|
||||
|
||||
# zoho mail config for production mode
|
||||
|
||||
# SENDER_PORT = 465
|
||||
# SENDER_SECURITY = true
|
||||
|
||||
SSL_CERT = "/home/ubuntu/spurrinai-backend-node/fullchain.pem"
|
||||
SSL_KEY = "/home/ubuntu/spurrinai-backend-node/privkey.pem"
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/node_modules
|
||||
/.env
|
||||
/hospital_data
|
||||
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)
|
||||
126
Jenkinsfile
vendored
Normal file
126
Jenkinsfile
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
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/Tech4Biz-Services/spurrin-cleaned-node.git'
|
||||
BRANCH = 'dev'
|
||||
|
||||
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('Clone Fresh Repo') {
|
||||
steps {
|
||||
echo '📁 Cloning fresh repo on remote via HTTPS...'
|
||||
withCredentials([usernamePassword(credentialsId: "${GIT_CREDENTIALS}", usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) {
|
||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
||||
sh """
|
||||
ssh ${REMOTE_SERVER} '
|
||||
set -e
|
||||
|
||||
# Backup entire uploads folder as folder inside uploads_backup
|
||||
if [ -d ${REMOTE_DIR}/uploads ]; then
|
||||
rm -rf ${BACKUP_UPLOADS_DIR}
|
||||
mkdir -p ${BACKUP_UPLOADS_DIR}
|
||||
cp -a ${REMOTE_DIR}/uploads ${BACKUP_UPLOADS_DIR}/
|
||||
fi
|
||||
|
||||
# Backup .env if it exists
|
||||
if [ -f ${REMOTE_DIR}/.env ]; then
|
||||
sudo cp ${REMOTE_DIR}/.env /tmp/.env
|
||||
fi
|
||||
|
||||
# Remove old repo and recreate directory
|
||||
rm -rf ${REMOTE_DIR}
|
||||
mkdir -p ${REMOTE_DIR}
|
||||
|
||||
# Clone fresh repo using HTTPS with creds
|
||||
git clone -b ${BRANCH} https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/Tech4Biz-Services/spurrin-cleaned-node.git ${REMOTE_DIR}
|
||||
|
||||
# Restore uploads folder as whole folder
|
||||
if [ -d ${BACKUP_UPLOADS_DIR}/uploads ]; then
|
||||
rm -rf ${REMOTE_DIR}/uploads
|
||||
cp -a ${BACKUP_UPLOADS_DIR}/uploads ${REMOTE_DIR}/
|
||||
fi
|
||||
|
||||
# Restore .env file
|
||||
if [ -f /tmp/.env ]; then
|
||||
sudo cp /tmp/.env ${REMOTE_DIR}/.env
|
||||
fi
|
||||
|
||||
# Copy certificates folder from home to repo
|
||||
if [ -d /home/ubuntu/certificates ]; then
|
||||
cp -a /home/ubuntu/certificates ${REMOTE_DIR}/
|
||||
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.
BIN
certificates.zip
Normal file
BIN
certificates.zip
Normal file
Binary file not shown.
48
certificates/fullchain.pem
Normal file
48
certificates/fullchain.pem
Normal file
@ -0,0 +1,48 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDujCCA0GgAwIBAgISBiFisWhUzGkrP0ZTJEh3wj37MAoGCCqGSM49BAMDMDIx
|
||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
|
||||
NTAeFw0yNTA0MjAxMzI4MThaFw0yNTA3MTkxMzI4MTdaMCAxHjAcBgNVBAMTFWJh
|
||||
Y2tlbmQuc3B1cnJpbmFpLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP1y
|
||||
P5gV5JqPW1ZY2XWLcOwhWIAveLAc/H4+i17T4/QikAwPkygW2d5yuT19wlUjj7mx
|
||||
EBsoi2h8O/vjFR2dDV2jggJHMIICQzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYw
|
||||
FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFB1x
|
||||
Pm735ggwPt/RA3LbpwvQZzeyMB8GA1UdIwQYMBaAFJ8rX888IU+dBLftKyzExnCL
|
||||
0tcNMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U1Lm8ubGVu
|
||||
Y3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vZTUuaS5sZW5jci5vcmcvMCAGA1Ud
|
||||
EQQZMBeCFWJhY2tlbmQuc3B1cnJpbmFpLmNvbTATBgNVHSAEDDAKMAgGBmeBDAEC
|
||||
ATAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vZTUuYy5sZW5jci5vcmcvMTI0LmNy
|
||||
bDCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AMz7D2qFcQll/pWbU87psnwi6YVc
|
||||
DZeNtql+VMD+TA2wAAABllOYP40AAAQDAEcwRQIhAMGo/Yvvtc46Da1K27DQjCCj
|
||||
h2HNr+rX24duotn40+KUAiBeezBfjnAexadpjDjq//7Heq6EFxq5PYOhNMMVY1HZ
|
||||
mwB2AN3cyjSV1+EWBeeVMvrHn/g9HFDf2wA6FBJ2Ciysu8gqAAABllOYP8EAAAQD
|
||||
AEcwRQIhAOJMMpK7EMKWl7c4ON8avkXTzuLgGBVSLp9MjuMqbV0IAiBYMbMb8/7L
|
||||
KMW5CigFocL1eIGVB2aCHfQeOSsaSRQg5jAKBggqhkjOPQQDAwNnADBkAjACpRDf
|
||||
C3d+OAd8fVD5ezqxdb9Hi2PU+tdj0CUnB4p8lwXfHP1Z6kcFn1G6ulIPhlwCMEIu
|
||||
eZmPVzb4HFcQHntkoJPuJBoGTFxEBhbzltCkgDjCN2cfEe0by6iuzCcNLTFyPw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEVzCCAj+gAwIBAgIRAIOPbGPOsTmMYgZigxXJ/d4wDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||
RW5jcnlwdDELMAkGA1UEAxMCRTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNCzqK
|
||||
a2GOtu/cX1jnxkJFVKtj9mZhSAouWXW0gQI3ULc/FnncmOyhKJdyIBwsz9V8UiBO
|
||||
VHhbhBRrwJCuhezAUUE8Wod/Bk3U/mDR+mwt4X2VEIiiCFQPmRpM5uoKrNijgfgw
|
||||
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSfK1/PPCFPnQS37SssxMZw
|
||||
i9LXDTAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
||||
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
||||
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
||||
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAH3KdNEVCQdqk0LKyuNImTKdRJY1C
|
||||
2uw2SJajuhqkyGPY8C+zzsufZ+mgnhnq1A2KVQOSykOEnUbx1cy637rBAihx97r+
|
||||
bcwbZM6sTDIaEriR/PLk6LKs9Be0uoVxgOKDcpG9svD33J+G9Lcfv1K9luDmSTgG
|
||||
6XNFIN5vfI5gs/lMPyojEMdIzK9blcl2/1vKxO8WGCcjvsQ1nJ/Pwt8LQZBfOFyV
|
||||
XP8ubAp/au3dc4EKWG9MO5zcx1qT9+NXRGdVWxGvmBFRAajciMfXME1ZuGmk3/GO
|
||||
koAM7ZkjZmleyokP1LGzmfJcUd9s7eeu1/9/eg5XlXd/55GtYjAM+C4DG5i7eaNq
|
||||
cm2F+yxYIPt6cbbtYVNJCGfHWqHEQ4FYStUyFnv8sjyqU8ypgZaNJ9aVcWSICLOI
|
||||
E1/Qv/7oKsnZCWJ926wU6RqG1OYPGOi1zuABhLw61cuPVDT28nQS/e6z95cJXq0e
|
||||
K1BcaJ6fJZsmbjRgD5p3mvEf5vdQM7MCEvU0tHbsx2I5mHHJoABHb8KVBgWp/lcX
|
||||
GWiWaeOyB7RP+OfDtvi2OsapxXiV7vNVs7fMlrRjY1joKaqmmycnBvAq14AEbtyL
|
||||
sVfOS66B8apkeFX2NY4XPEYV4ZSCe8VHPrdrERk2wILG3T/EGmSIkCYVUMSnjmJd
|
||||
VQD9F6Na/+zmXCc=
|
||||
-----END CERTIFICATE-----
|
||||
5
certificates/privkey.pem
Normal file
5
certificates/privkey.pem
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgT7PyDYzJD6fGTcZZ
|
||||
hjhX68XJO+epWhwx6fvZ3n9fOB2hRANCAAT9cj+YFeSaj1tWWNl1i3DsIViAL3iw
|
||||
HPx+Pote0+P0IpAMD5MoFtnecrk9fcJVI4+5sRAbKItofDv74xUdnQ1d
|
||||
-----END PRIVATE KEY-----
|
||||
2162
chat copy 2.py
Normal file
2162
chat copy 2.py
Normal file
File diff suppressed because it is too large
Load Diff
2043
chat copy 3.py
Normal file
2043
chat copy 3.py
Normal file
File diff suppressed because it is too large
Load Diff
1801
chat copy 4.py
Normal file
1801
chat copy 4.py
Normal file
File diff suppressed because it is too large
Load Diff
3857
chat copy 5.py
Normal file
3857
chat copy 5.py
Normal file
File diff suppressed because it is too large
Load Diff
2161
chat copy.py
Normal file
2161
chat copy.py
Normal file
File diff suppressed because it is too large
Load Diff
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`
|
||||
Binary file not shown.
64
logs/access.log
Normal file
64
logs/access.log
Normal file
@ -0,0 +1,64 @@
|
||||
2025-06-09 08:23:44,062 - INFO - "GET /flask-api/get-chroma-content" 404 - Duration: 4.423s - IP: 127.0.0.1
|
||||
2025-06-09 08:33:07,749 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 08:33:13,368 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 5.619s - IP: 127.0.0.1
|
||||
2025-06-09 08:35:59,718 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 10.160s - IP: 127.0.0.1
|
||||
2025-06-09 08:35:59,751 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 5.948s - IP: 127.0.0.1
|
||||
2025-06-09 08:36:33,990 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 1.606s - IP: 127.0.0.1
|
||||
2025-06-09 08:36:42,009 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.101s - IP: 127.0.0.1
|
||||
2025-06-09 08:36:47,823 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.262s - IP: 127.0.0.1
|
||||
2025-06-09 08:39:07,100 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.305s - IP: 127.0.0.1
|
||||
2025-06-09 08:39:17,353 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.679s - IP: 127.0.0.1
|
||||
2025-06-09 08:39:24,319 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.186s - IP: 127.0.0.1
|
||||
2025-06-09 08:40:47,458 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:02:11,699 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1284.241s - IP: 127.0.0.1
|
||||
2025-06-09 09:02:11,708 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:23:47,428 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1295.720s - IP: 127.0.0.1
|
||||
2025-06-09 09:23:47,438 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:23:48,194 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 0.756s - IP: 127.0.0.1
|
||||
2025-06-09 09:44:02,657 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:44:02,732 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:44:18,187 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 15.530s - IP: 127.0.0.1
|
||||
2025-06-09 09:44:18,916 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 16.183s - IP: 127.0.0.1
|
||||
2025-06-09 09:45:37,012 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:45:41,000 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 3.988s - IP: 127.0.0.1
|
||||
2025-06-09 09:45:41,005 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:45:41,996 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.402s - IP: 127.0.0.1
|
||||
2025-06-09 09:45:46,141 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 5.136s - IP: 127.0.0.1
|
||||
2025-06-09 09:45:46,147 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:45:50,933 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 4.786s - IP: 127.0.0.1
|
||||
2025-06-09 09:45:50,938 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:45:53,416 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 2.478s - IP: 127.0.0.1
|
||||
2025-06-09 09:45:53,424 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:12,535 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 19.111s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:12,542 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:14,676 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 2.135s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:14,682 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:15,923 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-09 09:46:16,338 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.657s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:18,617 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.694s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:34,853 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:34,854 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:34,880 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:34,881 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:34,917 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:34,917 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:34,952 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:34,953 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:35,033 - INFO - "DELETE /flask-api/delete-document-vectors" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:35,077 - INFO - "DELETE /flask-api/delete-document-vectors" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:35,109 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:35,154 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.045s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:35,205 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:35,245 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.040s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:35,301 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-09 09:46:38,883 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.582s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:38,924 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-09 09:46:44,600 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 5.677s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:47,483 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:47,824 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.341s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:47,868 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-09 09:46:47,868 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:47,899 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-09 09:46:52,833 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.933s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:52,876 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.009s - IP: 127.0.0.1
|
||||
2025-06-09 09:46:52,899 - INFO - "DELETE /flask-api/delete-document-vectors" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
82
logs/access.log.2025-06-06
Normal file
82
logs/access.log.2025-06-06
Normal file
@ -0,0 +1,82 @@
|
||||
2025-06-06 19:06:04,212 - INFO - "GET /flask-api/get-chroma-content" 404 - Duration: 0.006s - IP: 127.0.0.1
|
||||
2025-06-06 19:13:19,205 - INFO - "GET /flask-api/get-chroma-content" 404 - Duration: 0.004s - IP: 127.0.0.1
|
||||
2025-06-06 19:13:20,688 - INFO - "GET /flask-api/get-chroma-content" 404 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-06 19:50:30,860 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 19:50:30,911 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 19:50:42,056 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 11.145s - IP: 127.0.0.1
|
||||
2025-06-06 19:50:42,771 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 11.911s - IP: 127.0.0.1
|
||||
2025-06-06 19:50:59,869 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 19:51:02,219 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 2.350s - IP: 127.0.0.1
|
||||
2025-06-06 19:51:02,227 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 19:51:04,348 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.118s - IP: 127.0.0.1
|
||||
2025-06-06 20:07:53,888 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:07:53,978 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:02,783 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 8.895s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:04,362 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 10.384s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:21,553 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:23,788 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 2.235s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:23,794 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:25,591 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.005s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:28,422 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 4.627s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:28,430 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:34,785 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 6.355s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:34,791 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:38,443 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 3.652s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:38,449 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:42,138 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 3.689s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:42,146 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:45,506 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 3.361s - IP: 127.0.0.1
|
||||
2025-06-06 20:08:45,513 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:08:47,626 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 2.113s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:00,405 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:09:00,500 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:09:10,662 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 10.258s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:11,013 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 10.512s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:11,045 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:09:18,562 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 7.517s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:18,598 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:09:18,599 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:18,635 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:09:22,546 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.912s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:22,574 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:09:22,577 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.003s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:23,755 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 1.150s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:23,788 - INFO - "DELETE /flask-api/delete-document-vectors" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:23,819 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:09:32,675 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 8.856s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:32,712 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:09:39,875 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 7.163s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:39,902 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:09:39,982 - INFO - "POST /flask-api/process-pdf" 500 - Duration: 0.080s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:40,026 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:09:47,816 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 7.790s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:47,855 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:09:52,737 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.882s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:52,779 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:09:57,762 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.984s - IP: 127.0.0.1
|
||||
2025-06-06 20:09:57,788 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:10:02,717 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.929s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:07,418 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:10:07,424 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:10:07,426 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:10:07,428 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:10:07,436 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:10:10,074 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.646s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:10,115 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.691s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:10,129 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.703s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:10,900 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.464s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:11,015 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.597s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:11,252 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:10:14,893 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 3.641s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:14,927 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-06 20:10:14,927 - INFO - "POST /flask-api/process-pdf" 400 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:14,954 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:10:17,784 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.829s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:17,821 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.007s - IP: 127.0.0.1
|
||||
2025-06-06 20:10:17,851 - INFO - "DELETE /flask-api/delete-document-vectors" 400 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-06 20:30:35,675 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:30:35,707 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.032s - IP: 127.0.0.1
|
||||
2025-06-06 20:36:09,060 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:36:09,074 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.014s - IP: 127.0.0.1
|
||||
2025-06-06 20:38:19,169 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-06 20:38:19,180 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.011s - IP: 127.0.0.1
|
||||
129
logs/access.log.2025-06-07
Normal file
129
logs/access.log.2025-06-07
Normal file
@ -0,0 +1,129 @@
|
||||
2025-06-07 14:19:38,534 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-07 14:19:42,216 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 3.681s - IP: 127.0.0.1
|
||||
2025-06-07 14:20:17,322 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:20:23,302 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 5.980s - IP: 127.0.0.1
|
||||
2025-06-07 14:20:56,473 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:21:00,129 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.656s - IP: 127.0.0.1
|
||||
2025-06-07 14:21:23,752 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:21:28,209 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.457s - IP: 127.0.0.1
|
||||
2025-06-07 14:22:08,096 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:22:08,709 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.613s - IP: 127.0.0.1
|
||||
2025-06-07 14:22:28,281 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-07 14:34:42,442 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 734.161s - IP: 127.0.0.1
|
||||
2025-06-07 14:34:42,454 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-07 14:34:50,781 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 8.328s - IP: 127.0.0.1
|
||||
2025-06-07 14:35:29,997 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:35:31,844 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 1.847s - IP: 127.0.0.1
|
||||
2025-06-07 14:35:59,277 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:36:01,066 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 1.789s - IP: 127.0.0.1
|
||||
2025-06-07 14:36:17,780 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:36:19,922 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.143s - IP: 127.0.0.1
|
||||
2025-06-07 14:36:47,905 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 14:36:51,353 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.448s - IP: 127.0.0.1
|
||||
2025-06-07 15:17:00,219 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 15:17:04,769 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.551s - IP: 127.0.0.1
|
||||
2025-06-07 15:19:41,355 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 15:19:45,003 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.648s - IP: 127.0.0.1
|
||||
2025-06-07 15:20:11,644 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 15:20:16,053 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.408s - IP: 127.0.0.1
|
||||
2025-06-07 15:21:35,575 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 15:21:36,309 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.734s - IP: 127.0.0.1
|
||||
2025-06-07 17:20:04,262 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-07 17:27:08,688 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 17:27:12,411 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.722s - IP: 127.0.0.1
|
||||
2025-06-07 17:27:34,371 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 17:27:34,867 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.496s - IP: 127.0.0.1
|
||||
2025-06-07 17:27:57,600 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 17:27:59,935 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.336s - IP: 127.0.0.1
|
||||
2025-06-07 17:28:38,906 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 17:28:39,354 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 0.448s - IP: 127.0.0.1
|
||||
2025-06-07 17:28:51,317 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 17:28:53,646 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.330s - IP: 127.0.0.1
|
||||
2025-06-07 17:31:35,100 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 690.838s - IP: 127.0.0.1
|
||||
2025-06-07 17:31:35,113 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-07 17:31:44,097 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 8.984s - IP: 127.0.0.1
|
||||
2025-06-07 18:59:58,471 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.012s - IP: 127.0.0.1
|
||||
2025-06-07 18:59:59,489 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:00,496 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:00,510 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:20,087 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:21,096 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:22,104 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:22,112 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:38,374 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:39,383 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:40,388 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-07 19:00:40,394 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:03:14,959 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.004s - IP: 127.0.0.1
|
||||
2025-06-07 19:03:15,966 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-07 19:03:16,971 - INFO - "POST /flask-api/generate-answer" 500 - Duration: 0.002s - IP: 127.0.0.1
|
||||
2025-06-07 19:03:16,976 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:04:17,639 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:04:22,240 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.601s - IP: 127.0.0.1
|
||||
2025-06-07 19:05:27,746 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:05:32,436 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.690s - IP: 127.0.0.1
|
||||
2025-06-07 19:10:52,488 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:10:58,328 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 5.840s - IP: 127.0.0.1
|
||||
2025-06-07 19:11:22,941 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:11:24,101 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 1.159s - IP: 127.0.0.1
|
||||
2025-06-07 19:12:51,666 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:12:56,309 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.643s - IP: 127.0.0.1
|
||||
2025-06-07 19:14:06,125 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:14:10,531 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.407s - IP: 127.0.0.1
|
||||
2025-06-07 19:15:00,529 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:15:03,577 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.047s - IP: 127.0.0.1
|
||||
2025-06-07 19:15:50,924 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:15:53,124 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.199s - IP: 127.0.0.1
|
||||
2025-06-07 19:16:25,209 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:16:28,486 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.277s - IP: 127.0.0.1
|
||||
2025-06-07 19:17:28,778 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:17:30,340 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 1.562s - IP: 127.0.0.1
|
||||
2025-06-07 19:18:56,864 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:18:59,045 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.181s - IP: 127.0.0.1
|
||||
2025-06-07 19:29:40,049 - INFO - Generate answer request received from 127.0.0.1
|
||||
2025-06-07 19:29:41,121 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 1.072s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:19,246 - INFO - "POST /flask-api/generate-answer" 400 - Duration: 0.006s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:20,255 - INFO - "POST /flask-api/generate-answer" 400 - Duration: 0.003s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:21,263 - INFO - "POST /flask-api/generate-answer" 400 - Duration: 0.003s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:21,267 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:57,713 - INFO - "POST /flask-api/generate-answer" 400 - Duration: 0.004s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:58,728 - INFO - "POST /flask-api/generate-answer" 400 - Duration: 0.009s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:59,739 - INFO - "POST /flask-api/generate-answer" 400 - Duration: 0.005s - IP: 127.0.0.1
|
||||
2025-06-07 19:32:59,744 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:35:34,982 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 5.445s - IP: 127.0.0.1
|
||||
2025-06-07 19:35:38,404 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.416s - IP: 127.0.0.1
|
||||
2025-06-07 19:35:41,316 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.907s - IP: 127.0.0.1
|
||||
2025-06-07 19:35:41,321 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:36:12,080 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 6.126s - IP: 127.0.0.1
|
||||
2025-06-07 19:37:26,739 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.431s - IP: 127.0.0.1
|
||||
2025-06-07 19:38:04,883 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.114s - IP: 127.0.0.1
|
||||
2025-06-07 19:38:08,552 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.662s - IP: 127.0.0.1
|
||||
2025-06-07 19:38:11,796 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.240s - IP: 127.0.0.1
|
||||
2025-06-07 19:38:11,801 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:38:36,155 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.339s - IP: 127.0.0.1
|
||||
2025-06-07 19:39:13,327 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 4.803s - IP: 127.0.0.1
|
||||
2025-06-07 19:39:16,192 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.861s - IP: 127.0.0.1
|
||||
2025-06-07 19:39:19,105 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.906s - IP: 127.0.0.1
|
||||
2025-06-07 19:39:19,110 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:40:08,823 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 11.343s - IP: 127.0.0.1
|
||||
2025-06-07 19:40:11,863 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.022s - IP: 127.0.0.1
|
||||
2025-06-07 19:40:18,109 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 5.238s - IP: 127.0.0.1
|
||||
2025-06-07 19:40:18,122 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:40:51,855 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.262s - IP: 127.0.0.1
|
||||
2025-06-07 19:42:22,439 - INFO - "GET /flask-api/get-chroma-content" 404 - Duration: 0.016s - IP: 127.0.0.1
|
||||
2025-06-07 19:43:28,440 - INFO - "GET /flask-api/get-chroma-content" 200 - Duration: 0.309s - IP: 127.0.0.1
|
||||
2025-06-07 19:43:45,291 - INFO - "GET /flask-api/get-chroma-content" 200 - Duration: 0.236s - IP: 127.0.0.1
|
||||
2025-06-07 19:43:52,716 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.414s - IP: 127.0.0.1
|
||||
2025-06-07 19:44:54,322 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.739s - IP: 127.0.0.1
|
||||
2025-06-07 19:45:40,006 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.861s - IP: 127.0.0.1
|
||||
2025-06-07 19:45:43,191 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.179s - IP: 127.0.0.1
|
||||
2025-06-07 19:45:46,000 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.804s - IP: 127.0.0.1
|
||||
2025-06-07 19:45:46,008 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:46:31,772 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 1.854s - IP: 127.0.0.1
|
||||
2025-06-07 19:47:04,366 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.924s - IP: 127.0.0.1
|
||||
2025-06-07 19:47:07,391 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.021s - IP: 127.0.0.1
|
||||
2025-06-07 19:47:10,323 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.924s - IP: 127.0.0.1
|
||||
2025-06-07 19:47:10,329 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-07 19:49:11,578 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.438s - IP: 127.0.0.1
|
||||
2025-06-07 19:49:30,071 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.127s - IP: 127.0.0.1
|
||||
2025-06-07 20:11:16,636 - INFO - "GET /flask-api/get-chroma-content" 500 - Duration: 0.009s - IP: 127.0.0.1
|
||||
121
logs/access.log.2025-06-08
Normal file
121
logs/access.log.2025-06-08
Normal file
@ -0,0 +1,121 @@
|
||||
2025-06-08 11:50:24,602 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 11:50:30,729 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 6.127s - IP: 127.0.0.1
|
||||
2025-06-08 11:51:04,050 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 11:51:05,055 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.004s - IP: 127.0.0.1
|
||||
2025-06-08 11:51:24,470 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 11:51:25,174 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 0.704s - IP: 127.0.0.1
|
||||
2025-06-08 12:18:30,572 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 0.038s - IP: 127.0.0.1
|
||||
2025-06-08 12:18:31,590 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 0.006s - IP: 127.0.0.1
|
||||
2025-06-08 12:18:32,602 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 0.006s - IP: 127.0.0.1
|
||||
2025-06-08 12:18:32,611 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-08 12:21:38,813 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 4.478s - IP: 127.0.0.1
|
||||
2025-06-08 12:22:30,884 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.238s - IP: 127.0.0.1
|
||||
2025-06-08 12:22:56,397 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 4.158s - IP: 127.0.0.1
|
||||
2025-06-08 12:23:03,733 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 6.330s - IP: 127.0.0.1
|
||||
2025-06-08 12:25:40,160 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.393s - IP: 127.0.0.1
|
||||
2025-06-08 12:25:43,156 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.990s - IP: 127.0.0.1
|
||||
2025-06-08 12:25:46,163 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.003s - IP: 127.0.0.1
|
||||
2025-06-08 12:25:46,168 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:26:11,739 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.975s - IP: 127.0.0.1
|
||||
2025-06-08 12:26:14,926 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.184s - IP: 127.0.0.1
|
||||
2025-06-08 12:26:17,811 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.881s - IP: 127.0.0.1
|
||||
2025-06-08 12:26:17,815 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:27:11,475 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.928s - IP: 127.0.0.1
|
||||
2025-06-08 12:27:47,355 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 1.946s - IP: 127.0.0.1
|
||||
2025-06-08 12:31:36,960 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.967s - IP: 127.0.0.1
|
||||
2025-06-08 12:31:40,296 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.330s - IP: 127.0.0.1
|
||||
2025-06-08 12:31:43,335 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.034s - IP: 127.0.0.1
|
||||
2025-06-08 12:31:43,340 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:32:11,673 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.042s - IP: 127.0.0.1
|
||||
2025-06-08 12:32:14,932 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.255s - IP: 127.0.0.1
|
||||
2025-06-08 12:32:17,732 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.796s - IP: 127.0.0.1
|
||||
2025-06-08 12:32:17,737 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:33:17,499 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 5.376s - IP: 127.0.0.1
|
||||
2025-06-08 12:34:26,965 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.023s - IP: 127.0.0.1
|
||||
2025-06-08 12:34:29,735 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.766s - IP: 127.0.0.1
|
||||
2025-06-08 12:34:32,494 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.754s - IP: 127.0.0.1
|
||||
2025-06-08 12:34:32,498 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:46:29,718 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.230s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:28,427 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.133s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:31,363 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.931s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:34,473 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.105s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:34,479 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:52,307 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.362s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:55,171 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.858s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:57,971 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.796s - IP: 127.0.0.1
|
||||
2025-06-08 12:47:57,979 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:57:30,707 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.170s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:09,917 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.217s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:12,739 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.814s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:15,937 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.192s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:15,945 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:33,875 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.460s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:37,439 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.558s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:40,345 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.900s - IP: 127.0.0.1
|
||||
2025-06-08 12:58:40,352 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 12:59:23,266 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.766s - IP: 127.0.0.1
|
||||
2025-06-08 13:00:58,784 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 13:01:35,888 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 37.105s - IP: 127.0.0.1
|
||||
2025-06-08 13:01:35,909 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 13:01:43,172 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 7.264s - IP: 127.0.0.1
|
||||
2025-06-08 13:01:48,140 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 5.887s - IP: 127.0.0.1
|
||||
2025-06-08 13:01:50,871 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.724s - IP: 127.0.0.1
|
||||
2025-06-08 13:01:54,034 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.158s - IP: 127.0.0.1
|
||||
2025-06-08 13:01:54,040 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 13:06:18,133 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.120s - IP: 127.0.0.1
|
||||
2025-06-08 13:06:21,013 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.872s - IP: 127.0.0.1
|
||||
2025-06-08 13:06:24,343 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.324s - IP: 127.0.0.1
|
||||
2025-06-08 13:06:24,351 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.001s - IP: 127.0.0.1
|
||||
2025-06-08 13:06:39,661 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 13:06:40,982 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.321s - IP: 127.0.0.1
|
||||
2025-06-08 13:06:56,473 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.972s - IP: 127.0.0.1
|
||||
2025-06-08 13:06:59,283 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.804s - IP: 127.0.0.1
|
||||
2025-06-08 13:07:02,037 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.750s - IP: 127.0.0.1
|
||||
2025-06-08 13:07:02,042 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 13:07:59,953 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.856s - IP: 127.0.0.1
|
||||
2025-06-08 13:08:18,113 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.705s - IP: 127.0.0.1
|
||||
2025-06-08 13:08:48,711 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 2.996s - IP: 127.0.0.1
|
||||
2025-06-08 13:16:24,229 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.312s - IP: 127.0.0.1
|
||||
2025-06-08 13:16:27,166 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.932s - IP: 127.0.0.1
|
||||
2025-06-08 13:16:30,130 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.958s - IP: 127.0.0.1
|
||||
2025-06-08 13:16:30,135 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 13:19:37,502 - INFO - "POST /flask-api/generate-answer" 200 - Duration: 3.132s - IP: 127.0.0.1
|
||||
2025-06-08 14:09:56,029 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.797s - IP: 127.0.0.1
|
||||
2025-06-08 14:09:59,427 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 2.394s - IP: 127.0.0.1
|
||||
2025-06-08 14:10:02,376 - INFO - "POST /flask-api/generate-answer" 404 - Duration: 1.946s - IP: 127.0.0.1
|
||||
2025-06-08 14:10:02,381 - INFO - "POST /flask-api/self-generate-answer" 404 - Duration: 0.000s - IP: 127.0.0.1
|
||||
2025-06-08 18:43:30,814 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 18:45:44,482 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 133.668s - IP: 127.0.0.1
|
||||
2025-06-08 18:47:43,864 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 18:48:21,315 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 37.451s - IP: 127.0.0.1
|
||||
2025-06-08 18:54:20,485 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 18:54:27,286 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 6.801s - IP: 127.0.0.1
|
||||
2025-06-08 18:58:11,669 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 18:58:58,314 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 46.646s - IP: 127.0.0.1
|
||||
2025-06-08 19:12:52,812 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:13:00,169 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 7.358s - IP: 127.0.0.1
|
||||
2025-06-08 19:20:39,256 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:20:41,091 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.836s - IP: 127.0.0.1
|
||||
2025-06-08 19:22:32,024 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:22:33,176 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.152s - IP: 127.0.0.1
|
||||
2025-06-08 19:24:52,093 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.091s - IP: 127.0.0.1
|
||||
2025-06-08 19:25:03,560 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.109s - IP: 127.0.0.1
|
||||
2025-06-08 19:25:08,462 - INFO - "DELETE /flask-api/delete-document-vectors" 200 - Duration: 0.351s - IP: 127.0.0.1
|
||||
2025-06-08 19:25:52,369 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:25:54,085 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.717s - IP: 127.0.0.1
|
||||
2025-06-08 19:27:48,188 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:27:55,786 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 7.598s - IP: 127.0.0.1
|
||||
2025-06-08 19:32:45,783 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:32:52,772 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 6.989s - IP: 127.0.0.1
|
||||
2025-06-08 19:33:23,661 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:35:30,611 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 126.951s - IP: 127.0.0.1
|
||||
2025-06-08 19:37:07,682 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:37:08,846 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.164s - IP: 127.0.0.1
|
||||
2025-06-08 19:37:33,973 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 19:37:35,980 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 2.008s - IP: 127.0.0.1
|
||||
2025-06-08 21:32:25,360 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 21:32:27,854 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 2.494s - IP: 127.0.0.1
|
||||
2025-06-08 21:39:39,081 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 22:01:29,170 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1310.089s - IP: 127.0.0.1
|
||||
2025-06-08 22:01:29,182 - INFO - PDF processing request received from 127.0.0.1
|
||||
2025-06-08 22:01:30,353 - INFO - "POST /flask-api/process-pdf" 200 - Duration: 1.171s - IP: 127.0.0.1
|
||||
23743
logs/app.log
Normal file
23743
logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
21518
logs/combined.log
Normal file
21518
logs/combined.log
Normal file
File diff suppressed because one or more lines are too long
317
logs/error.log
Normal file
317
logs/error.log
Normal file
@ -0,0 +1,317 @@
|
||||
{"0":"r","1":"e","2":"a","3":"s","4":"o","5":"n","6":":","level":"error","message":"Unhandled Rejection at:","service":"spurrinai-backend","timestamp":"2025-06-06 18:30:26"}
|
||||
{"0":"r","1":"e","2":"a","3":"s","4":"o","5":"n","6":":","level":"error","message":"Unhandled Rejection at:","service":"spurrinai-backend","timestamp":"2025-06-06 18:31:24"}
|
||||
2025-06-06 19:50:30,865 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 19:50:30,957 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 19:50:59,876 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 19:51:02,234 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:07:53,915 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:07:54,029 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:08:21,559 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:08:23,800 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:08:28,435 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:08:34,799 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:08:38,462 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:08:42,151 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:08:45,519 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:09:00,442 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:09:00,652 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:09:11,086 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:09:23,863 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:09:39,907 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:09:39,908 - root - ERROR - [chat.py:447] - Error in extract_pdf_contents: Cannot read an empty file
|
||||
2025-06-06 20:09:39,908 - root - ERROR - [chat.py:1892] - Processing error: Cannot read an empty file
|
||||
2025-06-06 20:09:40,063 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-06 20:10:11,338 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-07 14:19:38,612 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-07 14:22:28,314 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-07 14:34:42,462 - root - ERROR - [chat.py:1812] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
{"ip":"::ffff:127.0.0.1","level":"error","message":"Not allowed by CORS","method":"OPTIONS","path":"/api/users/hospital-users/login","service":"spurrinai-backend","stack":"Error: Not allowed by CORS\n at origin (/home/ubuntu/spurrin-cleaned-node/src/middlewares/security.js:97:22)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:219:13\n at optionsCallback (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:199:9)\n at corsMiddleware (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:204:7)\n at Layer.handle [as handle_request] (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/layer.js:95:5)\n at trim_prefix (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:328:13)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:286:9\n at Function.process_params (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:346:12)\n at next (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:280:10)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express-rate-limit/dist/index.cjs:659:7","timestamp":"2025-06-07 15:17:42"}
|
||||
{"level":"error","message":"UNEXPECTED ERROR 💥 Not allowed by CORS","service":"spurrinai-backend","stack":"Error: Not allowed by CORS\n at origin (/home/ubuntu/spurrin-cleaned-node/src/middlewares/security.js:97:22)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:219:13\n at optionsCallback (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:199:9)\n at corsMiddleware (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:204:7)\n at Layer.handle [as handle_request] (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/layer.js:95:5)\n at trim_prefix (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:328:13)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:286:9\n at Function.process_params (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:346:12)\n at next (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:280:10)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express-rate-limit/dist/index.cjs:659:7","status":"error","statusCode":500,"timestamp":"2025-06-07 15:17:42"}
|
||||
{"ip":"::ffff:127.0.0.1","level":"error","message":"Not allowed by CORS","method":"OPTIONS","path":"/api/users/hospital-users/login","service":"spurrinai-backend","stack":"Error: Not allowed by CORS\n at origin (/home/ubuntu/spurrin-cleaned-node/src/middlewares/security.js:97:22)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:219:13\n at optionsCallback (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:199:9)\n at corsMiddleware (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:204:7)\n at Layer.handle [as handle_request] (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/layer.js:95:5)\n at trim_prefix (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:328:13)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:286:9\n at Function.process_params (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:346:12)\n at next (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:280:10)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express-rate-limit/dist/index.cjs:659:7","timestamp":"2025-06-07 15:17:43"}
|
||||
{"level":"error","message":"UNEXPECTED ERROR 💥 Not allowed by CORS","service":"spurrinai-backend","stack":"Error: Not allowed by CORS\n at origin (/home/ubuntu/spurrin-cleaned-node/src/middlewares/security.js:97:22)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:219:13\n at optionsCallback (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:199:9)\n at corsMiddleware (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:204:7)\n at Layer.handle [as handle_request] (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/layer.js:95:5)\n at trim_prefix (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:328:13)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:286:9\n at Function.process_params (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:346:12)\n at next (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:280:10)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express-rate-limit/dist/index.cjs:659:7","status":"error","statusCode":500,"timestamp":"2025-06-07 15:17:43"}
|
||||
{"ip":"::ffff:127.0.0.1","level":"error","message":"Not allowed by CORS","method":"OPTIONS","path":"/api/users/hospital-users/login","service":"spurrinai-backend","stack":"Error: Not allowed by CORS\n at origin (/home/ubuntu/spurrin-cleaned-node/src/middlewares/security.js:97:22)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:219:13\n at optionsCallback (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:199:9)\n at corsMiddleware (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:204:7)\n at Layer.handle [as handle_request] (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/layer.js:95:5)\n at trim_prefix (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:328:13)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:286:9\n at Function.process_params (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:346:12)\n at next (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:280:10)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express-rate-limit/dist/index.cjs:659:7","timestamp":"2025-06-07 15:18:01"}
|
||||
{"level":"error","message":"UNEXPECTED ERROR 💥 Not allowed by CORS","service":"spurrinai-backend","stack":"Error: Not allowed by CORS\n at origin (/home/ubuntu/spurrin-cleaned-node/src/middlewares/security.js:97:22)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:219:13\n at optionsCallback (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:199:9)\n at corsMiddleware (/home/ubuntu/spurrin-cleaned-node/node_modules/cors/lib/index.js:204:7)\n at Layer.handle [as handle_request] (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/layer.js:95:5)\n at trim_prefix (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:328:13)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:286:9\n at Function.process_params (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:346:12)\n at next (/home/ubuntu/spurrin-cleaned-node/node_modules/express/lib/router/index.js:280:10)\n at /home/ubuntu/spurrin-cleaned-node/node_modules/express-rate-limit/dist/index.cjs:659:7","status":"error","statusCode":500,"timestamp":"2025-06-07 15:18:01"}
|
||||
2025-06-07 17:20:04,305 - root - ERROR - [chat.py:1694] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-07 17:31:35,121 - root - ERROR - [chat.py:1694] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-07 18:59:58,460 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 18:59:59,488 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:00:00,495 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:00:20,085 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:00:21,094 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:00:22,103 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:00:38,372 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:00:39,382 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:00:40,387 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:03:14,956 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:03:15,964 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 19:03:16,970 - chat - ERROR - [app.py:875] - Exception on /flask-api/generate-answer [POST]
|
||||
Traceback (most recent call last):
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
|
||||
response = self.full_dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 919, in full_dispatch_request
|
||||
rv = self.handle_user_exception(e)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask_cors/extension.py", line 176, in wrapped_function
|
||||
return cors_after_request(app.make_response(f(*args, **kwargs)))
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
|
||||
rv = self.dispatch_request()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/venv/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
|
||||
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/ubuntu/spurrin-cleaned-node/chat.py", line 1449, in generate_answer
|
||||
question=question,
|
||||
TypeError: cannot unpack non-iterable coroutine object
|
||||
2025-06-07 20:11:16,627 - root - ERROR - [chat.py:3759] - Error in ChromaDB fetch process: invalid literal for int() with base 10: "229'"
|
||||
2025-06-08 11:50:24,616 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 11:51:04,055 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 11:51:24,473 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 13:00:58,804 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 13:01:35,921 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 13:06:39,668 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 18:43:30,831 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 18:47:43,882 - root - ERROR - [chat.py:3456] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 18:54:20,496 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 18:58:11,691 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:12:52,825 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:20:39,275 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:22:32,031 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:25:52,374 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:27:48,196 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:32:45,793 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:33:23,668 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:37:07,687 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 19:37:34,065 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 21:32:25,475 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 21:39:39,112 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-08 22:01:29,192 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 08:33:07,758 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 08:40:47,494 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:02:11,734 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:23:47,443 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:44:02,669 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:44:02,905 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:44:10,145 - root - ERROR - [chat.py:399] - Error saving ICD data to JSON for hospital 240: [Errno 2] No such file or directory: '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json.tmp' -> '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json'
|
||||
2025-06-09 09:44:10,148 - root - ERROR - [chat.py:399] - Error saving ICD data to JSON for hospital 240: [Errno 2] No such file or directory: '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json.tmp' -> '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json'
|
||||
2025-06-09 09:44:10,150 - root - ERROR - [chat.py:399] - Error saving ICD data to JSON for hospital 240: [Errno 2] No such file or directory: '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json.tmp' -> '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json'
|
||||
2025-06-09 09:44:10,150 - root - ERROR - [chat.py:399] - Error saving ICD data to JSON for hospital 240: [Errno 2] No such file or directory: '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json.tmp' -> '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json'
|
||||
2025-06-09 09:44:10,151 - root - ERROR - [chat.py:399] - Error saving ICD data to JSON for hospital 240: [Errno 2] No such file or directory: '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json.tmp' -> '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json'
|
||||
2025-06-09 09:44:10,154 - root - ERROR - [chat.py:399] - Error saving ICD data to JSON for hospital 240: [Errno 2] No such file or directory: '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json.tmp' -> '/home/ubuntu/spurrin-cleaned-node/hospital_data/hospital_240/icd_data.json'
|
||||
2025-06-09 09:45:37,019 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:45:41,012 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:45:46,151 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:45:50,943 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:45:53,436 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:46:12,551 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
2025-06-09 09:46:14,686 - root - ERROR - [chat.py:1299] - Database update error: (1265, "Data truncated for column 'processed_status' at row 1")
|
||||
0
logs/performance.log
Normal file
0
logs/performance.log
Normal file
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"
|
||||
}
|
||||
8257
package-lock.json
generated
Normal file
8257
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
|
||||
29
scripts/setup.js
Normal file
29
scripts/setup.js
Normal file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Create necessary directories
|
||||
const directories = [
|
||||
'logs',
|
||||
'uploads',
|
||||
'public/images',
|
||||
'tests/unit',
|
||||
'tests/integration',
|
||||
'docs'
|
||||
];
|
||||
|
||||
directories.forEach(dir => {
|
||||
const dirPath = path.join(__dirname, '..', dir);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`Created directory: ${dir}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Install dependencies
|
||||
console.log('Installing dependencies...');
|
||||
execSync('npm install', { stdio: 'inherit' });
|
||||
|
||||
console.log('Setup completed successfully!');
|
||||
2043
spurrin_testing_code.py
Normal file
2043
spurrin_testing_code.py
Normal file
File diff suppressed because it is too large
Load Diff
189
src/app.js
Normal file
189
src/app.js
Normal file
@ -0,0 +1,189 @@
|
||||
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();
|
||||
|
||||
console.log('Current Working Directory:', process.cwd());
|
||||
console.log('__dirname:', __dirname);
|
||||
|
||||
// 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 pdfRoutes = require('./routes/pdfRoutes');
|
||||
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/pdf', pdfRoutes);
|
||||
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 || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'spurrin',
|
||||
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: "smtp.zoho.com",
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER || "info@spurrin.com",
|
||||
pass: process.env.EMAIL_PASSWORD || "nbF314CF84Ja",
|
||||
},
|
||||
tls: {
|
||||
minVersion: "TLSv1.2",
|
||||
ciphers: "SSLv3",
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = transporter;
|
||||
51
src/config/env.js
Normal file
51
src/config/env.js
Normal file
@ -0,0 +1,51 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const env = {
|
||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
PORT: process.env.PORT || 3000,
|
||||
|
||||
// JWT Configuration
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '24h',
|
||||
|
||||
// Email Configuration
|
||||
EMAIL_USER: process.env.EMAIL_USER,
|
||||
EMAIL_PASS: process.env.EMAIL_PASS,
|
||||
EMAIL_HOST: process.env.EMAIL_HOST || 'smtp.gmail.com',
|
||||
EMAIL_PORT: process.env.EMAIL_PORT || 587,
|
||||
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
|
||||
SPURRIN_API_URL: process.env.SPURRIN_API_URL,
|
||||
SPURRIN_API_KEY: process.env.SPURRIN_API_KEY
|
||||
};
|
||||
|
||||
// Group required environment variables by feature
|
||||
const requiredEnvVars = {
|
||||
email: ['EMAIL_USER', 'EMAIL_PASS', 'EMAIL_HOST', 'BACK_URL'],
|
||||
database: ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'],
|
||||
jwt: ['JWT_SECRET'],
|
||||
api: ['SPURRIN_API_URL', 'SPURRIN_API_KEY']
|
||||
};
|
||||
|
||||
// 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
|
||||
};
|
||||
73
src/config/index.js
Normal file
73
src/config/index.js
Normal file
@ -0,0 +1,73 @@
|
||||
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'];
|
||||
330
src/config/initDatabase.js
Normal file
330
src/config/initDatabase.js
Normal file
@ -0,0 +1,330 @@
|
||||
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)
|
||||
)`,
|
||||
|
||||
// User sessions table
|
||||
`CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
role ENUM('hospital_user', 'super_admin', 'spurrin_admin') NOT NULL,
|
||||
status ENUM('loggedin', 'loggedout') NOT NULL DEFAULT 'loggedout',
|
||||
access_token VARCHAR(500) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
// Sessions table
|
||||
`CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
device_info VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
// 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.
457
src/controllers/analysisController.js
Normal file
457
src/controllers/analysisController.js
Normal file
@ -0,0 +1,457 @@
|
||||
const db = require('../config/database');
|
||||
|
||||
// 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!"
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
const response = {
|
||||
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
|
||||
}))
|
||||
};
|
||||
|
||||
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;
|
||||
console.log("start date--", start_date);
|
||||
console.log("end date---", end_date);
|
||||
|
||||
// Query to get hospitals that had any interaction in the selected period
|
||||
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 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
|
||||
// ${start_date && end_date ? 'WHERE il.created_at BETWEEN ? AND ?' : ''}
|
||||
// GROUP BY h.id
|
||||
// HAVING total_interactions > 0
|
||||
// ORDER BY total_interactions DESC
|
||||
// `;
|
||||
|
||||
const params = start_date && end_date ? [start_date, end_date] : [];
|
||||
const hospitals = await db.query(query, params);
|
||||
|
||||
// Get total count
|
||||
const totalCount = hospitals.length;
|
||||
|
||||
// Calculate total app users across all active hospitals
|
||||
const totalAppUsers = hospitals.reduce((sum, hospital) => sum + hospital.total_app_users, 0);
|
||||
|
||||
// Prepare response
|
||||
const response = {
|
||||
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
|
||||
}))
|
||||
};
|
||||
|
||||
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;
|
||||
console.log("start date--", start_date);
|
||||
console.log('end date---', end_date);
|
||||
|
||||
// Query to get active chat users and their details
|
||||
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);
|
||||
|
||||
// Get total count
|
||||
const totalCount = activeUsers.length;
|
||||
|
||||
// Calculate total interactions across all users
|
||||
const totalInteractions = activeUsers.reduce((sum, user) => sum + user.total_interactions, 0);
|
||||
|
||||
// Calculate average interactions per user
|
||||
const averageInteractions = totalCount > 0 ? (totalInteractions / totalCount).toFixed(2) : 0;
|
||||
|
||||
// Prepare response
|
||||
const response = {
|
||||
total_active_users: totalCount,
|
||||
total_interactions: totalInteractions,
|
||||
average_interactions_per_user: parseFloat(averageInteractions),
|
||||
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,
|
||||
first_interaction_date: user.first_interaction_date,
|
||||
last_interaction_date: user.last_interaction_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) => {
|
||||
console.log("req.user--", req.user);
|
||||
try {
|
||||
// Check authorization
|
||||
if(req.user.role !== 'Superadmin' && req.user.role !== 7){
|
||||
return res.status(403).json({
|
||||
error: "You are not authorized!"
|
||||
});
|
||||
}
|
||||
|
||||
const { start_date, end_date } = req.body;
|
||||
console.log("start date--", start_date);
|
||||
console.log("end date---", end_date);
|
||||
|
||||
// Query to get total hospital users for the specific hospital
|
||||
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 = [req.user.hospital_id];
|
||||
if (start_date && end_date) params.push(start_date, end_date);
|
||||
|
||||
const hospitals = await db.query(query, params);
|
||||
|
||||
if (hospitals.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: "Hospital not found"
|
||||
});
|
||||
}
|
||||
|
||||
const hospital = hospitals[0];
|
||||
|
||||
// Prepare response
|
||||
const response = {
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
message: "Hospital users analysis fetched successfully",
|
||||
data: response
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching hospital users analysis:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Get active users per hospital in selected period
|
||||
exports.getHospitalActiveUsers = async (req, res) => {
|
||||
try {
|
||||
// Check authorization
|
||||
if(req.user.role !== 'Superadmin' && req.user.role !== 6){
|
||||
return res.status(403).json({
|
||||
error: "You are not authorized!"
|
||||
});
|
||||
}
|
||||
|
||||
const { start_date, end_date } = req.body;
|
||||
console.log("start date--", start_date);
|
||||
console.log('end date---', end_date);
|
||||
|
||||
// Query to get active users per hospital in the selected period
|
||||
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);
|
||||
|
||||
// Get total count of hospitals
|
||||
const totalCount = hospitals.length;
|
||||
|
||||
// Calculate totals
|
||||
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);
|
||||
|
||||
// Prepare response
|
||||
const response = {
|
||||
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
|
||||
}))
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
message: "Hospital active users analysis fetched successfully",
|
||||
data: response
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching hospital active users analysis:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
3243
src/controllers/appUserController.js
Normal file
3243
src/controllers/appUserController.js
Normal file
File diff suppressed because it is too large
Load Diff
400
src/controllers/authController.js
Normal file
400
src/controllers/authController.js
Normal file
@ -0,0 +1,400 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const db = require("../config/database");
|
||||
const userService = require("../services/userService");
|
||||
const tokenService = require("../services/tokenService");
|
||||
const JWT_ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_TOKEN_SECRET;
|
||||
const JWT_REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_TOKEN_SECRET;
|
||||
const validRoles = ["Spurrinadmin", "Superadmin", "Admin", "Viewer"];
|
||||
const dotenv = require("dotenv");
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const JWT_ACCESS_TOKEN_EXPIRY = process.env.JWT_ACCESS_TOKEN_EXPIRY || "5h";
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
let table;
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);
|
||||
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";
|
||||
}
|
||||
|
||||
try {
|
||||
const id = decoded.id;
|
||||
|
||||
// Clear the refresh token for the user
|
||||
await db.query(`UPDATE ${table} SET access_token = NULL WHERE id = ?`, [
|
||||
id,
|
||||
]);
|
||||
|
||||
// await db.query(
|
||||
// "UPDATE sessions SET is_active = FALSE WHERE user_id = ? AND is_active = TRUE",
|
||||
// [id]
|
||||
// );
|
||||
|
||||
res.status(200).json({ message: "Logout successful!" });
|
||||
} 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 {
|
||||
// Verify the refresh token
|
||||
const decoded = jwt.verify(refreshToken, JWT_REFRESH_TOKEN_SECRET);
|
||||
const { email, role } = decoded;
|
||||
|
||||
console.log("decoded token ", decoded);
|
||||
|
||||
// Check if the user is already logged in
|
||||
|
||||
// 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) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ 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,
|
||||
});
|
||||
|
||||
// Update the access token in the hospital_users table
|
||||
// const expiryTimestamp = new Date();
|
||||
// expiryTimestamp.setMinutes(expiryTimestamp.getMinutes() + 15);
|
||||
|
||||
const expiryTimestamp = new Date();
|
||||
expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); // Add 5 hours
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE super_admins
|
||||
SET access_token = ?, access_token_expiry = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
await db.query(updateQuery, [newAccessToken, expiryTimestamp, user_id]);
|
||||
|
||||
// Respond with the new access token
|
||||
res.status(200).json({
|
||||
message: "Access token generated and updated successfully",
|
||||
accessToken: newAccessToken,
|
||||
user_id: user.id, // Optional: Include user ID in the response
|
||||
});
|
||||
} 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]; // Extract token from Authorization header
|
||||
|
||||
const { deviceInfo } = req.body;
|
||||
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 {
|
||||
// Validate the provided access token
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwt.verify(providedAccessToken, JWT_ACCESS_TOKEN_SECRET);
|
||||
} catch (err) {
|
||||
return res.status(403).json({ error: "Invalid or expired access token" });
|
||||
}
|
||||
|
||||
const { id, role } = decoded;
|
||||
|
||||
console.log("decoded ",decoded)
|
||||
console.log("decoded role",decoded.role)
|
||||
let subdomain;
|
||||
let hospitalData;
|
||||
let subdomain_data;
|
||||
|
||||
console.log("decoded token:", decoded);
|
||||
console.log("table ", role);
|
||||
// Check if the user exists in the appropriate table
|
||||
let table = role === "Spurrinadmin" ? "super_admins" : "hospital_users";
|
||||
|
||||
console.log("table---",table)
|
||||
console.log("role---",role)
|
||||
|
||||
let userQuery = `SELECT * FROM ${table} WHERE id = ?`;
|
||||
const userResult = await db.query(userQuery, [id]);
|
||||
const user = userResult[0];
|
||||
|
||||
if (table === "hospital_users") {
|
||||
let userQuery = `SELECT * FROM hospital_users WHERE id = ?`;
|
||||
const userResult = await db.query(userQuery, [id]);
|
||||
subdomain_data = userResult[0];
|
||||
|
||||
// Fetch subdomain from hospitals table based on hospital_code
|
||||
let hospitalQuery = `SELECT * FROM hospitals WHERE hospital_code = ?`;
|
||||
const hospitalResult = await db.query(hospitalQuery, [
|
||||
subdomain_data.hospital_code,
|
||||
]);
|
||||
hospitalData = hospitalResult[0];
|
||||
subdomain =
|
||||
hospitalResult.length > 0 ? hospitalResult[0].subdomain : null;
|
||||
console.log("Subdomain:", subdomain);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(403).json({ error: "Unauthorized access" });
|
||||
}
|
||||
|
||||
// Ensure the token matches the one stored in the database
|
||||
if (user.access_token !== providedAccessToken) {
|
||||
return res.status(403).json({ error: "Invalid or expired access token" });
|
||||
}
|
||||
|
||||
// Ensure the token has not expired
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(user.access_token_expiry);
|
||||
if (now > expiryDate) {
|
||||
return res.status(403).json({ error: "Access token has expired" });
|
||||
}
|
||||
|
||||
// Validate password
|
||||
const validPassword = await bcrypt.compare(
|
||||
password,
|
||||
user.hash_password || user.password
|
||||
);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ error: "Invalid email or password" });
|
||||
}
|
||||
|
||||
// Generate a new access token
|
||||
const payload = { id: user.id, email: user.email, role };
|
||||
const newAccessToken = jwt.sign(payload, JWT_ACCESS_TOKEN_SECRET, {
|
||||
expiresIn: "5h",
|
||||
});
|
||||
|
||||
// validateAndCreateSession(decoded.id, newAccessToken, deviceInfo)
|
||||
|
||||
// Update the access token in the database
|
||||
// const expiryTimestamp = new Date();
|
||||
// expiryTimestamp.setMinutes(expiryTimestamp.getMinutes() + 15);
|
||||
|
||||
const expiryTimestamp = new Date();
|
||||
expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); // Add 5 hours
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE ${table}
|
||||
SET access_token = ?, access_token_expiry = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
await db.query(updateQuery, [newAccessToken, expiryTimestamp, user.id]);
|
||||
|
||||
|
||||
// Build the response object
|
||||
|
||||
console.log("response: ", user);
|
||||
|
||||
console.log("hospital_data ", hospitalData);
|
||||
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,
|
||||
// name: user.admin_name,
|
||||
// profile_photo_url : result[0].profile_photo_url
|
||||
},
|
||||
accessToken: newAccessToken,
|
||||
};
|
||||
// Add hospital_id if the user is from hospital_users table
|
||||
console.log("table: ", table);
|
||||
if (table === "hospital_users") {
|
||||
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;
|
||||
}
|
||||
|
||||
// if (table === 'super_admins') {
|
||||
// response.user.name = user.hospital_id;
|
||||
// }
|
||||
|
||||
// Send response
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
console.error("Error during login:", error.message);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
async function validateAndCreateSession(userId, token, deviceInfo) {
|
||||
try {
|
||||
// Step 1: Invalidate previous session
|
||||
await db.query(
|
||||
"UPDATE sessions SET is_active = FALSE WHERE user_id = ? AND is_active = TRUE",
|
||||
[userId]
|
||||
);
|
||||
|
||||
// Step 2: Storing new session
|
||||
const [result] = await db.query(
|
||||
"INSERT INTO sessions (user_id, token, device_info, is_active) VALUES (?, ?, ?, TRUE)",
|
||||
[userId, token, deviceInfo]
|
||||
);
|
||||
|
||||
return { success: true, session: result };
|
||||
} catch (error) {
|
||||
console.error("Error creating session:", error);
|
||||
return { success: false, 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 {
|
||||
// Verify the token
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);
|
||||
const { id, role } = decoded;
|
||||
|
||||
// Extract hospital_id from request parameters and parse it to an integer
|
||||
const hospital_id = parseInt(req.params.hospital_id, 10);
|
||||
|
||||
if (isNaN(hospital_id)) {
|
||||
return res.status(400).json({ error: "Invalid hospital ID" });
|
||||
}
|
||||
|
||||
// Validate the user and access token against the hospital_users table
|
||||
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) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "You are not authorized to access this hospital" });
|
||||
}
|
||||
|
||||
// Ensure the token has not expired
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(user.access_token_expiry);
|
||||
if (now > expiryDate) {
|
||||
return res.status(403).json({ error: "Access token has expired" });
|
||||
}
|
||||
|
||||
// Attach user details to the request for further processing
|
||||
req.user = {
|
||||
id: user.id,
|
||||
hospital_id: user.hospital_id,
|
||||
role,
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error("Token verification error:", error.message);
|
||||
res.status(403).json({ error: "Invalid or expired access token" });
|
||||
}
|
||||
};
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
let table;
|
||||
const decoded = jwt.decode(token);
|
||||
|
||||
const id = decoded.id;
|
||||
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";
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db.query(
|
||||
`SELECT access_token FROM ${table} WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
console.log("table:",table,"result:",result)
|
||||
|
||||
if (result.length > 0 && result[0].access_token === token) {
|
||||
res.status(200).json({ message: "Token is active" });
|
||||
} else {
|
||||
res.status(400).json({ message: "Token not found or mismatched" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during token check:", error.message);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
449
src/controllers/documentsController.js
Normal file
449
src/controllers/documentsController.js
Normal file
@ -0,0 +1,449 @@
|
||||
const db = require('../config/database');
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
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('http://127.0.0.1:5000/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;
|
||||
|
||||
// Step 2: Send the file to the Python Flask API
|
||||
// const pythonApiUrl = 'http://127.0.0.1:5000/process-pdf';
|
||||
|
||||
|
||||
processDocumentFromPy(req.file, hospital_id, documentId, failed_page)
|
||||
|
||||
if (result || result.affectedRows > 0) {
|
||||
res.status(200).json({
|
||||
message: 'Document uploaded!',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
// console.error('Error uploading document:', error.message);
|
||||
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('http://localhost:5000/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' });
|
||||
}
|
||||
};
|
||||
234
src/controllers/exceldataController.js
Normal file
234
src/controllers/exceldataController.js
Normal file
@ -0,0 +1,234 @@
|
||||
const db = require('../config/database');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const sender_mail = process.env.mail;
|
||||
const sender_app_password = process.env.apppassword;
|
||||
const back_url = process.env.BACK_URL;
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: "smtp.zoho.com",
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: "no-reply@spurrin.com", // Your Zoho email address
|
||||
pass: "8TFvKswgH69Y", // Your Zoho App Password (not your account password)
|
||||
},
|
||||
// tls: {
|
||||
// rejectUnauthorized: false, // Allow self-signed certificates
|
||||
// minVersion: "TLSv1.2"
|
||||
// }
|
||||
});
|
||||
|
||||
// Create a new record
|
||||
exports.createExcelEntry = async (req, res) => {
|
||||
try {
|
||||
const requestorRole = req.user.role;
|
||||
const uploaded_by = req.user.id;
|
||||
const { hospital_id, hospital_code } = req.user;
|
||||
|
||||
|
||||
|
||||
if (!['Superadmin', 'Admin', 8, 7].includes(requestorRole)) {
|
||||
return res.status(403).json({ error: 'Access denied. Only Superadmin and Admin can do this action.' });
|
||||
}
|
||||
|
||||
const hospitalUsersQuery = `
|
||||
SELECT *
|
||||
FROM hospital_users
|
||||
WHERE hospital_id = ?
|
||||
`;
|
||||
const hospitalUserResult = await db.query(hospitalUsersQuery, [hospital_id]);
|
||||
|
||||
if (!hospitalUserResult || hospitalUserResult.length === 0) {
|
||||
return res.status(404).json({ error: 'Hospital not found for the given hospital_id' });
|
||||
}
|
||||
|
||||
// Ensure the request body is an array
|
||||
if (!Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: "Invalid data format. Expected an array." });
|
||||
}
|
||||
|
||||
const hospitalQuery = `
|
||||
SELECT *
|
||||
FROM hospitals
|
||||
WHERE hospital_code = ?
|
||||
`;
|
||||
const hospitalResult = await db.query(hospitalQuery, [hospital_code]);
|
||||
|
||||
sendEmails(req.body, hospitalResult, back_url);
|
||||
|
||||
|
||||
|
||||
|
||||
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 ?
|
||||
`;
|
||||
|
||||
// insert into hospital_users
|
||||
const values_hospital_users = await Promise.all(req.body.map(async (item) => {
|
||||
const hashedPassword = await bcrypt.hash(item.password, 10); // Hash the password
|
||||
|
||||
return [
|
||||
hospital_code,
|
||||
hospital_id,
|
||||
item.email,
|
||||
hashedPassword, // Use the hashed password here
|
||||
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]);
|
||||
console.log("result---", result)
|
||||
|
||||
// Generate and update refresh tokens for each inserted user
|
||||
// Get the first inserted ID and calculate subsequent IDs
|
||||
const firstInsertedId = result.insertId;
|
||||
const numberOfInsertedRows = result.affectedRows;
|
||||
|
||||
await Promise.all(
|
||||
req.body.map(async (item, index) => {
|
||||
const insertedUserId = firstInsertedId + index; // Calculate user ID
|
||||
|
||||
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]);
|
||||
})
|
||||
);
|
||||
// Constructing bulk insert query keeping a copy of uploaded users
|
||||
|
||||
|
||||
res.status(201).json({ message: "Records added successfully!" });
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error inserting data:", error.message);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Retrieve all records
|
||||
|
||||
async function sendEmails(users, hospitalResult, back_url) {
|
||||
|
||||
for (const user of users) {
|
||||
const mailOptions = {
|
||||
from: "no-reply@spurrin.com", // Sender's email
|
||||
to: user.email, // Unique recipient email
|
||||
subject: 'Spurrinai Login Credentials', // Email subject
|
||||
html: `<!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}'public/images/email-banner.png'); background-size: cover; background-position: right bottom; border-radius: 8px;">
|
||||
</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, ${user.name},</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;">${hospitalResult[0].name_hospital}</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;">${hospitalResult[0].name_hospital}</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;">${hospitalResult[0].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;">${user.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;">${user.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://${hospitalResult[0].subdomain}user" 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>`
|
||||
};
|
||||
|
||||
try {
|
||||
await transporter.sendMail(mailOptions);
|
||||
} catch (error) {
|
||||
console.error(`Error sending email to ${user.email_id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
411
src/controllers/feedbacksController.js
Normal file
411
src/controllers/feedbacksController.js
Normal file
@ -0,0 +1,411 @@
|
||||
const db = require('../config/database');
|
||||
|
||||
// Create feedback from app user to hospital
|
||||
exports.createAppUserFeedback = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
hospital_code,
|
||||
rating,
|
||||
purpose,
|
||||
information_received,
|
||||
feedback_text,
|
||||
improvement,
|
||||
} = req.body;
|
||||
const user_id = req.user.id; // From auth middleware
|
||||
|
||||
console.log(
|
||||
'user data---',
|
||||
hospital_code,
|
||||
rating,
|
||||
purpose,
|
||||
information_received,
|
||||
feedback_text,
|
||||
improvement
|
||||
);
|
||||
|
||||
// Validate required fields
|
||||
if (!hospital_code) {
|
||||
return res.status(400).json({
|
||||
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) {
|
||||
return res.status(404).json({
|
||||
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,
|
||||
]);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Feedback submitted successfully',
|
||||
feedback_id: result.insertId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating app user feedback:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Create feedback from hospital to Spurrin
|
||||
exports.createHospitalFeedback = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
rating,
|
||||
purpose,
|
||||
information_received,
|
||||
feedback_text,
|
||||
improvement
|
||||
} = req.body;
|
||||
const hospital_code = req.user.hospital_code; // From auth middleware
|
||||
|
||||
// Validate required fields
|
||||
if (!rating || !purpose || !information_received) {
|
||||
return res.status(400).json({
|
||||
error: "Rating, purpose and information received are required"
|
||||
});
|
||||
}
|
||||
|
||||
// Validate rating enum
|
||||
const validRating = ['angry', 'sad', 'neutral', 'happy', 'awesome'];
|
||||
if (!validRating.includes(rating)) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid rating value"
|
||||
});
|
||||
}
|
||||
|
||||
// Validate information_received enum
|
||||
const validInfoReceived = ['Yes', 'Partially', 'No'];
|
||||
if (!validInfoReceived.includes(information_received)) {
|
||||
return res.status(400).json({
|
||||
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) {
|
||||
return res.status(404).json({
|
||||
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
|
||||
]);
|
||||
|
||||
res.status(201).json({
|
||||
message: "Feedback submitted successfully",
|
||||
feedback_id: result.insertId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating hospital feedback:", error);
|
||||
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; // From auth middleware
|
||||
|
||||
// Get hospital ID
|
||||
const hospitalCheck = await db.query(
|
||||
'SELECT id FROM hospitals WHERE hospital_code = ?',
|
||||
[hospital_code]
|
||||
);
|
||||
|
||||
if (hospitalCheck.length === 0) {
|
||||
return res.status(404).json({
|
||||
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]);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Feedbacks fetched successfully",
|
||||
data: feedbacks
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching hospital feedbacks:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
// Get all feedbacks (for Spurrin admin)
|
||||
exports.getAllFeedbacks = 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 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);
|
||||
|
||||
res.status(200).json({
|
||||
message: "All feedbacks fetched successfully",
|
||||
data: feedbacks
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching all feedbacks:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
// Forward app user feedbacks to Spurrin (for hospital users)
|
||||
exports.forwardAppUserFeedbacks = async (req, res) => {
|
||||
try {
|
||||
const { feedback_ids } = req.body;
|
||||
const hospital_code = req.user.hospital_code;
|
||||
|
||||
if (!feedback_ids || !Array.isArray(feedback_ids) || feedback_ids.length === 0) {
|
||||
return res.status(400).json({ error: "Feedback IDs array is required" });
|
||||
}
|
||||
|
||||
const hospitalCheck = await db.query(
|
||||
'SELECT id FROM hospitals WHERE hospital_code = ?',
|
||||
[hospital_code]
|
||||
);
|
||||
|
||||
if (hospitalCheck.length === 0) {
|
||||
return res.status(404).json({ 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) {
|
||||
return res.status(400).json({
|
||||
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);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Feedbacks forwarded to Spurrin successfully",
|
||||
forwarded_count: feedback_ids.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error forwarding feedbacks:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
// API to get all forwarded feedbacks for Spurrin
|
||||
exports.getForwardedFeedbacks = 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 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);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Forwarded feedbacks fetched successfully.",
|
||||
data: forwardedFeedbacks
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching forwarded feedbacks:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
1685
src/controllers/hospitalController.js
Normal file
1685
src/controllers/hospitalController.js
Normal file
File diff suppressed because it is too large
Load Diff
171
src/controllers/onboardingController.js
Normal file
171
src/controllers/onboardingController.js
Normal file
@ -0,0 +1,171 @@
|
||||
const db = require('../config/database');
|
||||
|
||||
exports.getOnboardingSteps = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params; // Extract user ID from the request
|
||||
|
||||
|
||||
// Validate the authenticated user is a Superadmin
|
||||
if (req.user.role !== 'Superadmin') {
|
||||
return res.status(403).json({ error: 'You are not authorized to fetch onboarding steps' });
|
||||
}
|
||||
|
||||
// Ensure the authenticated user's ID matches the userId from the URL
|
||||
if (req.user.id !== parseInt(userId, 10)) {
|
||||
return res.status(403).json({
|
||||
error: 'You are not authorized to fetch onboarding steps for this user',
|
||||
});
|
||||
}
|
||||
|
||||
if (!userId || isNaN(userId)) {
|
||||
return res.status(404).json({ '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 res.status(404).json({ steps: 'Pending' });
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Onboarding steps fetched successfully!', data: steps });
|
||||
} catch (error) {
|
||||
console.error('Error fetching onboarding steps:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.addOnboardingStep = async (req, res) => {
|
||||
try {
|
||||
const { userId, step } = req.body;
|
||||
|
||||
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 (req.user.role !== 'Superadmin') {
|
||||
return res.status(403).json({ 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) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const user = userValidationResult[0];
|
||||
|
||||
// Ensure the user belongs to the same hospital, if applicable
|
||||
if (req.user.hospital_id && req.user.hospital_id !== user.hospital_id) {
|
||||
return res.status(403).json({ 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) {
|
||||
return res.status(400).json({ error: 'Failed to add onboarding step' });
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
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) {
|
||||
return res.status(400).json({ error: 'No changes made to the onboarding step' });
|
||||
}
|
||||
|
||||
return res.status(200).json({ message: 'Onboarding step updated successfully!' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding onboarding step:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateOnboardingStep = async (req, res) => {
|
||||
try {
|
||||
const { user_id } = req.params; // The user ID for the onboarding step to be updated
|
||||
const { step } = req.body;
|
||||
|
||||
|
||||
// Validate that the authenticated user is a Superadmin
|
||||
if (req.user.role !== 'Superadmin') {
|
||||
return res.status(403).json({ error: 'You are not authorized to update onboarding steps' });
|
||||
}
|
||||
|
||||
// Ensure the authenticated user's ID matches the user_id from the URL
|
||||
if (req.user.id !== parseInt(user_id, 10)) {
|
||||
return res.status(403).json({
|
||||
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, [user_id]);
|
||||
|
||||
if (!onboardingStepResult || onboardingStepResult.length === 0) {
|
||||
return res.status(404).json({ 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, user_id]);
|
||||
|
||||
if (updateResult.affectedRows === 0) {
|
||||
return res.status(400).json({ error: 'No changes made to the onboarding step' });
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Onboarding step updated successfully!' });
|
||||
} catch (error) {
|
||||
console.error('Error updating onboarding step:', 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 });
|
||||
}
|
||||
};
|
||||
612
src/controllers/superAdminController.js
Normal file
612
src/controllers/superAdminController.js
Normal file
@ -0,0 +1,612 @@
|
||||
const superAdminService = require('../services/superAdminService');
|
||||
const bcrypt = require('bcrypt');
|
||||
//const tokenService = require('../services/tokenService');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const db = require('../config/database');
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: "smtp.zoho.com", // Zoho SMTP Server
|
||||
port: 465, // Use 465 for SSL or 587 for TLS
|
||||
secure: true, // Set to true for port 465, false for port 587
|
||||
auth: {
|
||||
user: "no-reply@spurrin.com", // Your Zoho email address
|
||||
pass: "8TFvKswgH69Y", // Your Zoho App Password (not your account password)
|
||||
},
|
||||
// tls: {
|
||||
// minVersion: "TLSv1.2",
|
||||
// ciphers: "SSLv3",
|
||||
// },
|
||||
});
|
||||
|
||||
//sign up call for Spurrinadmin
|
||||
exports.initializeSuperAdmin = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: "Email and password are required" });
|
||||
}
|
||||
|
||||
// Check if a SuperAdmin with the same email already exists
|
||||
const existingAdminQuery = 'SELECT id FROM super_admins WHERE email = ?';
|
||||
const existingAdminResult = await db.query(existingAdminQuery, [email]);
|
||||
|
||||
if (existingAdminResult.length > 0) {
|
||||
return res.status(400).json({ error: 'SuperAdmin with this email already exists' });
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Insert the new SuperAdmin into the database **before** generating the access token
|
||||
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; // ✅ Ensure we get the correct ID
|
||||
|
||||
// 🔹 **Ensure `id` is included in the JWT payload**
|
||||
const payload = {
|
||||
id: superAdminId, // ✅ Add `id`
|
||||
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' });
|
||||
|
||||
// Set Access Token Expiry
|
||||
const expiryTimestamp = new Date();
|
||||
expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); // Add 5 hours
|
||||
|
||||
// Update the tokens in the database
|
||||
const updateQuery = `
|
||||
UPDATE super_admins
|
||||
SET refresh_token = ?, access_token = ?, access_token_expiry = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
await db.query(updateQuery, [refreshToken, accessToken, expiryTimestamp, superAdminId]);
|
||||
|
||||
// ✅ **Return the correct ID in response**
|
||||
res.status(201).json({
|
||||
message: 'SuperAdmin created successfully',
|
||||
user: {
|
||||
id: superAdminId,
|
||||
email: email,
|
||||
role: 'Spurrinadmin',
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing SuperAdmin:', 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];
|
||||
|
||||
if (!accessToken) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify the access token
|
||||
const decoded = jwt.verify(accessToken, process.env.JWT_ACCESS_TOKEN_SECRET);
|
||||
const { id, role } = decoded;
|
||||
|
||||
if (role !== 'Spurrinadmin') {
|
||||
return res.status(403).json({ error: 'Unauthorized role for this API' });
|
||||
}
|
||||
|
||||
// Validate the token in the database
|
||||
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) {
|
||||
return res.status(403).json({ error: 'Unauthorized access' });
|
||||
}
|
||||
|
||||
if (superAdmin.access_token !== accessToken) {
|
||||
return res.status(403).json({ error: 'Invalid or expired access token' });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(superAdmin.access_token_expiry);
|
||||
if (now > expiryDate) {
|
||||
return res.status(403).json({ error: 'Access token has expired' });
|
||||
}
|
||||
|
||||
// Fetch all super admins
|
||||
const fetchAdminsQuery = 'SELECT id, email, role_id FROM super_admins';
|
||||
const superAdmins = await db.query(fetchAdminsQuery);
|
||||
|
||||
res.status(200).json(superAdmins);
|
||||
} catch (error) {
|
||||
console.error('Error fetching SuperAdmins:', error.message);
|
||||
res.status(403).json({ error: 'Invalid or expired access token' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.addSuperAdmin = async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Check if a SuperAdmin with the same email exists
|
||||
const checkAdminQuery = 'SELECT id FROM super_admins WHERE email = ?';
|
||||
const existingAdmin = await db.query(checkAdminQuery, [email]);
|
||||
|
||||
if (existingAdmin.length > 0) {
|
||||
return res.status(400).json({ error: 'SuperAdmin with this email already exists' });
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Insert the new super admin **before** generating tokens
|
||||
const insertAdminQuery = `
|
||||
INSERT INTO super_admins (email, password, role_id)
|
||||
VALUES (?, ?, ?)
|
||||
`;
|
||||
const result = await db.query(insertAdminQuery, [
|
||||
email,
|
||||
hashedPassword,
|
||||
6, // Assuming 6 is the role ID for Spurrinadmin
|
||||
]);
|
||||
|
||||
// ✅ Now, get the new user ID
|
||||
const newSuperAdminId = result.insertId;
|
||||
|
||||
// ✅ Generate JWT tokens **after** user is inserted
|
||||
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);
|
||||
|
||||
// ✅ Set token expiry timestamp
|
||||
const expiryTimestamp = new Date();
|
||||
expiryTimestamp.setHours(expiryTimestamp.getHours() + 5);
|
||||
|
||||
// ✅ Now update the tokens in the database
|
||||
const updateTokensQuery = `
|
||||
UPDATE super_admins
|
||||
SET refresh_token = ?, access_token = ?, access_token_expiry = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
await db.query(updateTokensQuery, [refreshToken, accessToken, expiryTimestamp, newSuperAdminId]);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'SuperAdmin added successfully',
|
||||
user: {
|
||||
id: newSuperAdminId,
|
||||
email,
|
||||
role: 'Spurrinadmin',
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error adding SuperAdmin:', 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) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// change password
|
||||
const crypto = require("crypto");
|
||||
|
||||
function generateRandomPassword(length = 12) {
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString("hex")
|
||||
.slice(0, length);
|
||||
}
|
||||
|
||||
exports.sendTempPassword = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
// Validate email input
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: "Email is required" });
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
const user = await db.query(
|
||||
"SELECT id, email FROM super_admins WHERE email = ?",
|
||||
[email]
|
||||
);
|
||||
|
||||
if (!user.length) {
|
||||
return res.status(404).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
|
||||
const superAdminId = user[0].id;
|
||||
|
||||
const randomPassword = generateRandomPassword();
|
||||
// const hashedPassword = await bcrypt.hash(randomPassword, 10);
|
||||
const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000); // OTP expires in 1 hour
|
||||
const type = "temp";
|
||||
|
||||
await db.query(
|
||||
"UPDATE super_admins SET temporary_password= ?, expires_at = ?, type = ? WHERE id = ?",
|
||||
[randomPassword, expiresAt, type, superAdminId]
|
||||
);
|
||||
|
||||
|
||||
// // // Send OTP via email
|
||||
const info =await sendMail(
|
||||
email,
|
||||
'Spurrin',
|
||||
user[0].email,
|
||||
randomPassword
|
||||
);
|
||||
res.json({ message: "temporary password generated successfully", email_status: info.response });
|
||||
} catch (error) {
|
||||
console.error("Error sending OTP:", error);
|
||||
res.status(500).json({ error: error });
|
||||
}
|
||||
};
|
||||
|
||||
exports.changeTempPassword = async (req, res) => {
|
||||
try {
|
||||
const { email, temp_password, new_password } = req.body;
|
||||
|
||||
// Validate inputs
|
||||
if (!email || !temp_password || !new_password) {
|
||||
return res.status(400).json({
|
||||
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) {
|
||||
return res.status(404).json({ error: "User not found" });
|
||||
}
|
||||
|
||||
|
||||
|
||||
const isMatch = temp_password === user[0].temporary_password;
|
||||
|
||||
|
||||
// Check if temporary password matches
|
||||
if (!isMatch) {
|
||||
return res.status(400).json({ error: "Invalid temporary password" });
|
||||
}
|
||||
|
||||
// Check if temporary password is expired
|
||||
if (new Date() > new Date(user[0].expires_at)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ 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]
|
||||
);
|
||||
|
||||
|
||||
res.json({ message: "Password changed successfully!" });
|
||||
} catch (error) {
|
||||
console.error("Error resetting password:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
async function sendMail(email, hospital_name, adminName, randomPassword) {
|
||||
const mailOptions = {
|
||||
from: 'no-reply@spurrin.com', // Sender's email
|
||||
to: email, // Recipient's email
|
||||
subject: "Spurrinai temporary password", // Email subject
|
||||
html: `<!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 - 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>`,
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
return info;
|
||||
} catch (error) {
|
||||
console.error(`Error sending email to ${email}:`, error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
exports.getDataConsumptionReport = async (req, res) => {
|
||||
|
||||
console.log("requested user is ",req.user)
|
||||
|
||||
if(req.user.role !== 'Spurrinadmin' && req.user.role !== 6){
|
||||
return res.status(403).json({
|
||||
error: "You are not authorized!"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// 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);
|
||||
|
||||
// Prepare the response
|
||||
const report = {
|
||||
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
|
||||
}))
|
||||
};
|
||||
|
||||
res.status(200).json(report);
|
||||
} catch (error) {
|
||||
console.error('Error generating data consumption report:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getOnboardedHospitals = 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!"
|
||||
});
|
||||
}
|
||||
|
||||
// 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 res.status(200).json({
|
||||
message: "No onboarded hospitals found",
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: "Onboarded hospitals fetched successfully",
|
||||
data: hospitals
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching onboarded hospitals:", error);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
};
|
||||
908
src/controllers/userController.js
Normal file
908
src/controllers/userController.js
Normal file
@ -0,0 +1,908 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const db = require('../config/database');
|
||||
const tokenService = require('../services/tokenService');
|
||||
const multer = require('multer');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const path = require('path');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
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 sender_mail = process.env.mail;
|
||||
const back_url = process.env.BACK_URL;
|
||||
|
||||
|
||||
|
||||
// zoho transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: "smtp.zoho.com",
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: "no-reply@spurrin.com", // Your Zoho email address
|
||||
pass: "8TFvKswgH69Y", // Your Zoho App Password (not your account password)
|
||||
},
|
||||
// tls: {
|
||||
// rejectUnauthorized: false, // Allow self-signed certificates
|
||||
// minVersion: "TLSv1.2"
|
||||
// }
|
||||
});
|
||||
|
||||
// Utility function to resolve the table name based on role_id
|
||||
const 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');
|
||||
};
|
||||
|
||||
|
||||
exports.addUser = async (req, res) => {
|
||||
try {
|
||||
let hospitalResult;
|
||||
const { hospital_id, role_id, ...rest } = req.body;
|
||||
const requestorRole = req.user.role; // Extracted from the authenticated user's token
|
||||
const requestorHospitalId = req.user.hospital_id; // Extracted from the authenticated user's token
|
||||
// Step 1: Validate the role of the requestor
|
||||
if (!['Superadmin', 'Admin', 7, 8].includes(requestorRole)) {
|
||||
return res.status(403).json({ error: 'Access denied. Only Superadmin and Admin can add users.' });
|
||||
}
|
||||
|
||||
if (![8, 9].includes(role_id)) {
|
||||
return res.status(403).json({ error: `Access denied, cannot add user with role_id ${role_id}` });
|
||||
}
|
||||
|
||||
// Step 2: Validate the hospital_id
|
||||
if (hospital_id !== requestorHospitalId) {
|
||||
return res.status(403).json({
|
||||
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, [rest.email]);
|
||||
|
||||
if (spurrinEmailResult.length > 0) {
|
||||
return res.status(403).json({ error: "Email already exists!" });
|
||||
}
|
||||
|
||||
const hsptUsrEmailQuery =
|
||||
"SELECT email from hospital_users WHERE email = ?";
|
||||
const hsptUsrEmailResult = await db.query(hsptUsrEmailQuery, [rest.email]);
|
||||
|
||||
if (hsptUsrEmailResult.length > 0) {
|
||||
return res.status(403).json({ error: "Email already exists!" });
|
||||
}
|
||||
|
||||
// step
|
||||
const hospitalQuery = `
|
||||
SELECT *
|
||||
FROM hospitals
|
||||
WHERE id = ?
|
||||
`;
|
||||
hospitalResult = await db.query(hospitalQuery, [hospital_id]);
|
||||
|
||||
if (!hospitalResult || hospitalResult.length === 0) {
|
||||
return res.status(404).json({ 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) {
|
||||
return res.status(404).json({ 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 = resolveTableName(role_id);
|
||||
|
||||
// Step 4: Hash the password
|
||||
const passwordHash = await bcrypt.hash(req.body.password, 10);
|
||||
|
||||
const mailOptions = {
|
||||
from: "no-reply@spurrin.com", // Sender's email
|
||||
to: rest.email, // Unique recipient email
|
||||
subject: 'Spurrinai Login Credentials', // Email subject
|
||||
html: `<!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}'public/images/email-banner.png'); background-size: cover; background-position: right bottom; border-radius: 8px;">
|
||||
</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, ${rest.name},</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;">${hospitalResult[0].name_hospital}</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;">${hospitalResult[0].name_hospital}</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;">${hospitalResult[0].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;">${rest.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;">${rest.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://${hospitalResult[0].subdomain}user" 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>`
|
||||
};
|
||||
|
||||
// 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,city,mobile_number)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?,?,?,?,?)
|
||||
`;
|
||||
const result = await db.query(query, [
|
||||
hospital_code,
|
||||
hospital_id,
|
||||
rest.email,
|
||||
passwordHash,
|
||||
role_id,
|
||||
rest.is_default_admin,
|
||||
0,
|
||||
rest.password_reset_required,
|
||||
profile_photo_url,
|
||||
rest.phone_number,
|
||||
rest.bio,
|
||||
rest.status,
|
||||
rest.name,
|
||||
rest.department,
|
||||
rest.location,
|
||||
rest.city,
|
||||
rest.mobile_number
|
||||
]);
|
||||
|
||||
|
||||
// Step 6: Generate tokens for the new user
|
||||
const payload = { id: result.insertId, email: rest.email, role: role_id };
|
||||
const accessToken = tokenService.generateAccessToken(payload);
|
||||
const refreshToken = tokenService.generateRefreshToken(payload);
|
||||
const expiryTimestamp = new Date();
|
||||
expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); // Add 5 hours
|
||||
|
||||
// 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]);
|
||||
|
||||
// step mail along with login cridentials
|
||||
if (!rest.email) {
|
||||
return res.status(400).json({ error: 'Recipient email is required' });
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
message: 'User added successfully!',
|
||||
user: { id: result.insertId, role_id: role_id, ...rest },
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
responseData["Email info"] = info.response;
|
||||
} catch (emailError) {
|
||||
console.error("Email sending failed:", emailError.message);
|
||||
responseData["Email info"] = "Email sending failed: " + emailError.message;
|
||||
}
|
||||
|
||||
// Step 3: Send response regardless of email outcome
|
||||
res.status(201).json(responseData);
|
||||
} catch (error) {
|
||||
console.error('Error adding user:', error.message);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUsersByHospital = async (req, res) => {
|
||||
const hospital_id = parseInt(req.params.hospital_id, 10);
|
||||
|
||||
if (isNaN(hospital_id)) {
|
||||
return res.status(400).json({ error: 'Invalid hospital ID' });
|
||||
}
|
||||
|
||||
// Ensure the authenticated user has access to the requested hospital
|
||||
if (
|
||||
(req.user.role === 'Admin' && req.user.hospital_id !== hospital_id) ||
|
||||
(req.user.role === 'Superadmin' && req.user.hospital_id !== hospital_id) ||
|
||||
(req.user.role === 8 && req.user.hospital_id !== hospital_id) ||
|
||||
(req.user.role === 9 && req.user.hospital_id !== hospital_id)
|
||||
) {
|
||||
return res.status(403).json({ error: 'You are not authorized to access this hospital' });
|
||||
}
|
||||
try {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM hospital_users
|
||||
WHERE hospital_id = ?
|
||||
`;
|
||||
const users = await db.query(query, [hospital_id]);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Users fetched successfully',
|
||||
users,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getProfilePhoto = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id; // Assuming user ID is from the authenticated token
|
||||
|
||||
|
||||
// Fetch the profile photo URL from the database
|
||||
const query = 'SELECT profile_photo_url FROM hospital_users WHERE id = ?';
|
||||
const result = await db.query(query, [userId]);
|
||||
|
||||
if (
|
||||
(req.user.role === 'Admin') ||
|
||||
(req.user.role === 'Superadmin') ||
|
||||
(req.user.role === 8)
|
||||
) {
|
||||
return res.status(403).json({ error: 'You are not authorized to access this hospital' });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return res.status(404).json({ error: 'Profile photo not found' });
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Profile photo fetched successfully!',
|
||||
profile_photo_url: result[0].profile_photo_url,
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching profile photo:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.login = async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await userService.findUserByEmail(email);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid email or password' });
|
||||
}
|
||||
|
||||
const isValidPassword = await bcrypt.compare(password, user.hash_password);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ 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 userService.updateRefreshToken(user.id, refreshToken);
|
||||
|
||||
res.status(200).json({ accessToken, refreshToken });
|
||||
} catch (error) {
|
||||
console.error('Login error:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.logout = async (req, res) => {
|
||||
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
console.log('token', token)
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
|
||||
// Verify the token
|
||||
const decoded = jwt.decode(token);
|
||||
console.log('Decoded Token:', decoded);
|
||||
|
||||
try {
|
||||
const { id, table } = decoded; // Extract user details from the token
|
||||
|
||||
|
||||
|
||||
// Ensure the table is valid
|
||||
if (!['Spurrinadmin', 'Superadmin', 'Admin', 'Viewer', 6, 7, 8, 9].includes(decoded.role)) {
|
||||
return res.status(403).json({ error: 'Unauthorized access' });
|
||||
}
|
||||
|
||||
const deleteQuery = 'DELETE FROM user_sessions WHERE user_id = ?';
|
||||
const result = await db.query(deleteQuery, [id]);
|
||||
|
||||
|
||||
res.status(200).json({ message: 'Logout successful!' });
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', 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'); // Set the destination folder
|
||||
},
|
||||
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');
|
||||
|
||||
// API to upload 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 {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
const userId = req.user.id; // Assuming user ID is from authenticated token
|
||||
const photoPath = `/uploads/profile_photos/${req.file.filename}`;
|
||||
|
||||
// Update the photo URL in the database
|
||||
await db.query(
|
||||
'UPDATE hospital_users SET profile_photo_url = ? WHERE id = ?',
|
||||
[photoPath, userId]
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Profile photo uploaded successfully!',
|
||||
profile_photo_url: photoPath,
|
||||
});
|
||||
} 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; // Hospital user ID
|
||||
const updatedData = req.body; // Fields to update
|
||||
const requestorRole = req.user.role;
|
||||
const requestorHospitalId = req.user.hospital_id; // Extracted from the authenticated user's token
|
||||
const { hospital_id, role_id, ...rest } = req.body;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'User ID is required' });
|
||||
}
|
||||
|
||||
// Step 1: Validate the role of the requestor
|
||||
if (!['Superadmin', 'Admin', 'Viewer', 8, 9, 7].includes(requestorRole)) {
|
||||
return res.status(403).json({ error: 'Access denied. Only Superadmin, user and Admin can update users.' });
|
||||
}
|
||||
|
||||
// Step 2: Validate the hospital_id
|
||||
// if (String(hospital_id).toString().trim() !== String(requestorHospitalId).toString().trim()) {
|
||||
// return res.status(403).json({
|
||||
// error: 'Access denied. You can only edit users of your hospital.',
|
||||
// });
|
||||
// }
|
||||
|
||||
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',
|
||||
'city',
|
||||
'mobile_number',
|
||||
'access_token',
|
||||
'access_token_expiry',
|
||||
'hospital_code'
|
||||
];
|
||||
|
||||
// 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) {
|
||||
return res.status(400).json({ 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);
|
||||
|
||||
// to update user role
|
||||
// Step 6: Generate tokens for the new user
|
||||
const payload = { id: result.insertId, email: rest.email, role: role_id };
|
||||
const accessToken = tokenService.generateAccessToken(payload);
|
||||
const refreshToken = tokenService.generateRefreshToken(payload);
|
||||
|
||||
|
||||
// Step 7: Store the refresh token in the database
|
||||
const updateQuery = `UPDATE hospital_users SET refresh_token = ?, access_token = ? WHERE id = ?`;
|
||||
await db.query(updateQuery, [refreshToken, accessToken, result.insertId]);
|
||||
//
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ error: 'Hospital user not found' });
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Hospital user updated successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error editing hospital user:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteHospitalUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const requestorRole = req.user.role;
|
||||
const hospitalId = req.user.hospital_id;
|
||||
const requestorHospitalId = req.user.hospital_id; // Extracted from the authenticated user's token
|
||||
// const { hospital_id, role_id, ...rest } = req.body;
|
||||
|
||||
|
||||
|
||||
console.log("user data,", req.user)
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: 'User ID is required' });
|
||||
}
|
||||
|
||||
// Step 1: Validate the role of the requestor
|
||||
if (!['Superadmin', 'Admin', 8, 7].includes(requestorRole)) {
|
||||
return res.status(403).json({ error: 'Access denied. Only Superadmin and Admin can delete users.' });
|
||||
}
|
||||
|
||||
// Step 2: Validate the hospital_id
|
||||
// if (String(hospital_id).toString().trim() !== String(requestorHospitalId).toString().trim()) {
|
||||
// return res.status(403).json({
|
||||
// error: 'Access denied. You can only delete members of your hospital.',
|
||||
// });
|
||||
// }
|
||||
|
||||
// Check for dependent records
|
||||
const [qaCount] = await db.query(
|
||||
"SELECT COUNT(*) AS count FROM questions_answers WHERE document_id IN (SELECT id FROM documents WHERE uploaded_by = ?)",
|
||||
[id]
|
||||
);
|
||||
|
||||
const [qaPageCount] = await db.query(
|
||||
"SELECT COUNT(*) AS count FROM document_pages WHERE document_id IN (SELECT id FROM documents WHERE uploaded_by = ?)",
|
||||
[id]
|
||||
);
|
||||
const [metadataCount] = await db.query(
|
||||
"SELECT COUNT(*) AS count FROM document_metadata WHERE document_id IN (SELECT id FROM documents WHERE uploaded_by = ?)",
|
||||
[id]
|
||||
);
|
||||
const [documentsCount] = await db.query(
|
||||
"SELECT COUNT(*) AS count FROM documents WHERE uploaded_by = ?",
|
||||
[id]
|
||||
);
|
||||
|
||||
// point to be discussed or resolved
|
||||
// const [appUsersCount] = await db.query(
|
||||
// "SELECT COUNT(*) AS count FROM app_users WHERE hospital_code = (SELECT hospital_code FROM hospitals WHERE id = ?)",
|
||||
// [hospitalId]
|
||||
// );
|
||||
|
||||
|
||||
// console.log('qaCount', qaCount,'\nqaPageCount', qaPageCount, '\nmetadataCount', metadataCount, '\ndocumentsCount', documentsCount, '\nappUsersCount', appUsersCount);
|
||||
|
||||
// If any dependent records exist, block deletion
|
||||
if (qaCount.count > 0 || metadataCount.count > 0 || documentsCount.count > 0 || qaPageCount.count > 0) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Can not delete hospital dependent records found" });
|
||||
}
|
||||
|
||||
|
||||
// then delete hospital user
|
||||
const query = `DELETE FROM hospital_users WHERE id = ?`;
|
||||
const result = await db.query(query, [id]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ error: 'Hospital user not found' });
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Hospital user deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting hospital user:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getAccessToken = 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 {
|
||||
// Verify the refresh token
|
||||
const decoded = jwt.verify(refreshToken, JWT_REFRESH_TOKEN_SECRET);
|
||||
const { email, role } = decoded;
|
||||
|
||||
console.log("decoded ---", 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) {
|
||||
return res.status(403).json({ 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,
|
||||
});
|
||||
|
||||
// Update the access token in the hospital_users table
|
||||
// const expiryTimestamp = new Date();
|
||||
// expiryTimestamp.setMinutes(expiryTimestamp.getMinutes() + 15);
|
||||
|
||||
const expiryTimestamp = new Date();
|
||||
expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); // Add 5 hours
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE hospital_users
|
||||
SET access_token = ?, access_token_expiry = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
await db.query(updateQuery, [newAccessToken, expiryTimestamp, user_id]);
|
||||
|
||||
// Respond with the new access token
|
||||
res.status(200).json({
|
||||
message: 'Access token generated and updated successfully',
|
||||
accessToken: newAccessToken,
|
||||
user_id: user.id, // Optional: Include user ID in the response
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating access token:', error.message);
|
||||
res.status(500).json({ error: 'Invalid or expired refresh token' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getAccessTokenForSpurrinadmin = 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 {
|
||||
// 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) {
|
||||
return res.status(403).json({ 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,
|
||||
});
|
||||
|
||||
// Update the access token in the hospital_users table
|
||||
// const expiryTimestamp = new Date();
|
||||
// expiryTimestamp.setMinutes(expiryTimestamp.getMinutes() + 15);
|
||||
|
||||
const expiryTimestamp = new Date();
|
||||
expiryTimestamp.setHours(expiryTimestamp.getHours() + 5); // Add 5 hours
|
||||
|
||||
const updateQuery = `
|
||||
UPDATE super_admins
|
||||
SET access_token = ?, access_token_expiry = ?
|
||||
WHERE id = ?
|
||||
`;
|
||||
await db.query(updateQuery, [newAccessToken, expiryTimestamp, user_id]);
|
||||
|
||||
// Respond with the new access token
|
||||
res.status(200).json({
|
||||
message: 'Access token generated and updated successfully',
|
||||
accessToken: newAccessToken,
|
||||
user_id: user.id, // Optional: Include user ID in the response
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating access token:', error.message);
|
||||
res.status(403).json({ error: 'Invalid or expired refresh token' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getRefreshTokenByUserId = async (req, res) => {
|
||||
try {
|
||||
const { user_id, role_id } = req.params; // Accept both user_id and role_id from request params
|
||||
|
||||
let table;
|
||||
let roleName;
|
||||
|
||||
// ✅ Step 1: 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) { // Add any other hospital roles if needed
|
||||
table = 'hospital_users';
|
||||
roleName = 'HospitalUser';
|
||||
} else {
|
||||
return res.status(400).json({ error: "Invalid role_id provided" });
|
||||
}
|
||||
|
||||
|
||||
// ✅ Step 2: Fetch refresh token from the selected table
|
||||
const query = `SELECT refresh_token FROM ${table} WHERE id = ?`;
|
||||
const result = await db.query(query, [user_id]);
|
||||
|
||||
// ✅ Step 3: Handle cases where user is not found
|
||||
if (!result || result.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found or no refresh token available' });
|
||||
}
|
||||
|
||||
const refreshToken = result[0].refresh_token;
|
||||
|
||||
// ✅ Step 4: Return refresh token
|
||||
res.status(200).json({
|
||||
message: 'Refresh token fetched successfully',
|
||||
user_id: user_id,
|
||||
role_id: role_id,
|
||||
role_name: roleName,
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching refresh token:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getHospitalUserId = async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 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) {
|
||||
return res.status(404).json({ 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) {
|
||||
return res.status(401).json({ error: 'Invalid email or password' });
|
||||
}
|
||||
|
||||
// If match, return the user ID, role ID, and role name
|
||||
res.status(200).json({ userId: user.id, roleId: user.role_id, roleName: user.role_name });
|
||||
} catch (error) {
|
||||
console.error('Error fetching hospital user:', error.message);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.updatePassword = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params; // Get the user ID from the route parameter
|
||||
const { new_password } = req.body; // Get the new password from the request body
|
||||
const authHeader = req.headers.authorization; // Get the token from the Authorization header
|
||||
|
||||
|
||||
// Validate input
|
||||
if (!new_password) {
|
||||
return res.status(400).json({ error: 'New password is required' });
|
||||
}
|
||||
|
||||
// Ensure the token is present
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Authorization token is required' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1]; // Extract token from "Bearer <token>"
|
||||
let decodedToken;
|
||||
try {
|
||||
decodedToken = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET); // Decode the token
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
|
||||
// Ensure the decoded token's user ID matches the route parameter
|
||||
if (parseInt(id, 10) !== decodedToken.id) {
|
||||
return res.status(403).json({ error: 'Token user does not match the requested user' });
|
||||
}
|
||||
|
||||
// Convert ID to integer and validate
|
||||
const numericId = parseInt(id, 10);
|
||||
if (isNaN(numericId)) {
|
||||
return res.status(400).json({ 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]);
|
||||
|
||||
console.log("user result-----", userResult)
|
||||
|
||||
if (!userResult || userResult.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const existingHashedPassword = userResult.hash_password;
|
||||
const isSamePassword = await bcrypt.compare(new_password, existingHashedPassword);
|
||||
|
||||
if (isSamePassword) {
|
||||
return res.status(400).json({ 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]);
|
||||
|
||||
res.status(200).json({ message: 'Password updated successfully!' });
|
||||
} catch (error) {
|
||||
console.error('Error updating password:', error.message, error.stack);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports
|
||||
267
src/middlewares/authMiddleware.js
Normal file
267
src/middlewares/authMiddleware.js
Normal file
@ -0,0 +1,267 @@
|
||||
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.authenticateSession = async (req, res, next) => {
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
let token = refreshToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: "Access token is required" });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_REFRESH_TOKEN_SECRET);
|
||||
|
||||
|
||||
const userId = decoded.id;
|
||||
|
||||
// Step 1: Check for an active session
|
||||
const session = await db.query(
|
||||
"SELECT * FROM sessions WHERE user_id = ? AND is_active = TRUE",
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (session.length === 0) {
|
||||
// First-time login: Create a new session automatically
|
||||
const newSession = await db.query(
|
||||
"INSERT INTO sessions (user_id, token, device_info, is_active) VALUES (?, ?, ?, TRUE)",
|
||||
[userId, token, req.headers["user-agent"]]
|
||||
);
|
||||
|
||||
req.session = newSession;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Verify the session token
|
||||
if (session[0].token == token) {
|
||||
|
||||
return res.status(401).json({
|
||||
message: "second device detected!!",
|
||||
device : session[0]
|
||||
});
|
||||
}
|
||||
|
||||
req.session = decoded;
|
||||
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.authenticateSessionForAppUsers = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(" ")[1]; // Extract token from header
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: "Access token is required"});
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);
|
||||
|
||||
const userId = decoded.id;
|
||||
|
||||
// Step 1: Check for an active session
|
||||
const session = await db.query(
|
||||
"SELECT * FROM sessions WHERE user_id = ? AND is_active = TRUE",
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (session.length === 0) {
|
||||
// First-time login: Create a new session automatically
|
||||
const newSession = await db.query(
|
||||
"INSERT INTO sessions (user_id, token, device_info, is_active) VALUES (?, ?, ?, TRUE)",
|
||||
[userId, token, req.headers["user-agent"]]
|
||||
);
|
||||
|
||||
req.session = newSession;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Verify the session token
|
||||
if (session[0].token !== token) {
|
||||
|
||||
return res.status(401).json({
|
||||
message: "second device detected!! action required!!",
|
||||
device : session[0]
|
||||
});
|
||||
}
|
||||
|
||||
req.session = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(403).json({ error: "Invalid or expired access token" });
|
||||
}
|
||||
};
|
||||
|
||||
/*********************************************************************
|
||||
* Company: Tech4biz Solutions
|
||||
* Author: Tech4biz Solutions team backend
|
||||
* Description: Authenticates user based on roles
|
||||
* Copyright: Copyright © 2025Tech4Biz Solutions.
|
||||
*********************************************************************/
|
||||
32
src/middlewares/authorizeMiddleware.js
Normal file
32
src/middlewares/authorizeMiddleware.js
Normal file
@ -0,0 +1,32 @@
|
||||
const { ForbiddenError } = require('../utils/errors');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Middleware to authorize users based on their roles
|
||||
* @param {string[]} allowedRoles - Array of roles that are allowed to access the route
|
||||
* @returns {Function} Express middleware function
|
||||
*/
|
||||
const authorize = (allowedRoles) => {
|
||||
return (req, res, next) => {
|
||||
try {
|
||||
// Check if user exists in request (set by authenticate middleware)
|
||||
if (!req.user) {
|
||||
throw new ForbiddenError('User not authenticated');
|
||||
}
|
||||
|
||||
// Check if user has required role
|
||||
if (!allowedRoles.includes(req.user.role)) {
|
||||
logger.warn(`Unauthorized access attempt by user ${req.user.id} with role ${req.user.role}`);
|
||||
throw new ForbiddenError('You do not have permission to perform this action');
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authorize
|
||||
};
|
||||
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
|
||||
};
|
||||
52
src/middlewares/profilePhotoUploadMiddleware.js
Normal file
52
src/middlewares/profilePhotoUploadMiddleware.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;
|
||||
19
src/middlewares/roleMiddleware.js
Normal file
19
src/middlewares/roleMiddleware.js
Normal file
@ -0,0 +1,19 @@
|
||||
// middlewares/roleMiddleware.js
|
||||
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: Checks and allows only allowed roles
|
||||
* Copyright: Copyright © 2025Tech4Biz Solutions.
|
||||
*********************************************************************/
|
||||
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
|
||||
};
|
||||
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;
|
||||
47
src/migrations/createMigration.js
Normal file
47
src/migrations/createMigration.js
Normal file
@ -0,0 +1,47 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
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');
|
||||
}
|
||||
};
|
||||
40
src/models/hospitalModel.js
Normal file
40
src/models/hospitalModel.js
Normal file
@ -0,0 +1,40 @@
|
||||
const db = require('../config/database');
|
||||
|
||||
|
||||
exports.insertHospital = async (data) => {
|
||||
const { name_hospital, subdomain, primary_admin_email, primary_admin_password, primary_color, secondary_color, logo_url } = data;
|
||||
const result = await db.query(
|
||||
`INSERT INTO hospitals (name_hospital, subdomain, primary_admin_email, primary_admin_password, primary_color, secondary_color, logo_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name_hospital, subdomain, primary_admin_email, primary_admin_password, primary_color, secondary_color, logo_url]
|
||||
);
|
||||
return { id: result.insertId, name_hospital, logo_url };
|
||||
};
|
||||
|
||||
exports.updateHospitalLogo = async (hospitalId, logoUrl) => {
|
||||
try {
|
||||
const result = await db.query(
|
||||
'UPDATE hospitals SET logo_url = ? WHERE id = ?',
|
||||
[logoUrl, hospitalId]
|
||||
);
|
||||
if (result.affectedRows > 0) {
|
||||
return { message: 'Logo updated successfully' }; // Return a success message if update is successful
|
||||
} else {
|
||||
throw new Error('Hospital not found or no changes made');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating hospital logo:', error.message);
|
||||
throw new Error('Error updating hospital logo');
|
||||
}
|
||||
};
|
||||
|
||||
// Insert a new hospital (this method might be part of your existing implementation)
|
||||
exports.insertHospital = async (data) => {
|
||||
const { name_hospital, subdomain, primary_admin_email, primary_admin_password, primary_color, secondary_color, logo_url } = data;
|
||||
const result = await db.query(
|
||||
`INSERT INTO hospitals (name_hospital, subdomain, primary_admin_email, primary_admin_password, primary_color, secondary_color, logo_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name_hospital, subdomain, primary_admin_email, primary_admin_password, primary_color, secondary_color, logo_url]
|
||||
);
|
||||
return { id: result.insertId, name_hospital, logo_url };
|
||||
};
|
||||
5
src/models/roleModel.js
Normal file
5
src/models/roleModel.js
Normal file
@ -0,0 +1,5 @@
|
||||
const db = require('../config/database');
|
||||
|
||||
exports.getAllRoles = async () => {
|
||||
return await db.query('SELECT * FROM roles');
|
||||
};
|
||||
15
src/models/superAdminModel.js
Normal file
15
src/models/superAdminModel.js
Normal file
@ -0,0 +1,15 @@
|
||||
const db = require('../config/database');
|
||||
|
||||
exports.getAllSuperAdmins = async () => {
|
||||
return await db.query('SELECT * FROM super_admins');
|
||||
};
|
||||
|
||||
exports.addSuperAdmin = async (data) => {
|
||||
const { email, passwordHash } = data;
|
||||
const result = await db.query('INSERT INTO super_admins (email, hash_password) VALUES (?, ?)', [email, passwordHash]);
|
||||
return { id: result.insertId, email };
|
||||
};
|
||||
|
||||
exports.deleteSuperAdmin = async (id) => {
|
||||
await db.query('DELETE FROM super_admins WHERE id = ?', [id]);
|
||||
};
|
||||
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;
|
||||
164
src/routes/appUsers.js
Normal file
164
src/routes/appUsers.js
Normal file
@ -0,0 +1,164 @@
|
||||
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;
|
||||
70
src/routes/documents.js
Normal file
70
src/routes/documents.js
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const authMiddleware = require('../middlewares/authMiddleware');
|
||||
const roleMiddleware = require('../middlewares/roleMiddleware');
|
||||
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,
|
||||
roleMiddleware.authorizeRoles(['Superadmin', 'Admin', 'Viewer',8,9,7]),
|
||||
documentController.getDocumentsByHospital
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/update-status/:id',
|
||||
authMiddleware.authenticateToken,
|
||||
roleMiddleware.authorizeRoles(['Superadmin', 'Admin',8,7]),
|
||||
documentController.updateDocumentStatus
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/delete/:id',
|
||||
authMiddleware.authenticateToken,
|
||||
roleMiddleware.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;
|
||||
23
src/routes/feedbacks.js
Normal file
23
src/routes/feedbacks.js
Normal file
@ -0,0 +1,23 @@
|
||||
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)
|
||||
|
||||
module.exports = router;
|
||||
223
src/routes/hospitals.js
Normal file
223
src/routes/hospitals.js
Normal file
@ -0,0 +1,223 @@
|
||||
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 hospitalModel = require("../models/hospitalModel"); // Ensure the model is imported correctly
|
||||
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 uploading hospital logo
|
||||
router.post(
|
||||
"/upload-logo",
|
||||
authMiddleware.authenticateToken, // Middleware to validate access token
|
||||
upload.single("logo"), // Multer middleware to handle single file upload
|
||||
async (req, res) => {
|
||||
try {
|
||||
// Extract JWT token from headers
|
||||
const authHeader = req.headers["authorization"];
|
||||
const token = authHeader && authHeader.split(" ")[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: "Access token required" });
|
||||
}
|
||||
|
||||
// Verify the token
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);
|
||||
const { id, role, email } = decoded; // Extract user ID, role, and email from the decoded token
|
||||
|
||||
// Check if a file is uploaded
|
||||
if (!req.file) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "No file uploaded or invalid field name" });
|
||||
}
|
||||
|
||||
// File URL with original extension
|
||||
const logoUrl = `/uploads/logos/${req.file.filename}`;
|
||||
|
||||
// Fetch hospital data for the user (assuming the user is related to a hospital)
|
||||
|
||||
const hospitalquery = `SELECT * FROM hospital_users WHERE id = ?`;
|
||||
|
||||
const [hospital] = await db.query(hospitalquery, [id]);
|
||||
|
||||
// If no hospital is found, return an error
|
||||
if (!hospital || hospital.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ error: "Hospital not found for this user" });
|
||||
}
|
||||
// Update hospital with new logo URL
|
||||
const updatedHospital = await hospitalModel.updateHospitalLogo(
|
||||
hospital.hospital_id,
|
||||
logoUrl
|
||||
);
|
||||
|
||||
// Return success message with updated hospital data
|
||||
res.status(200).json({
|
||||
message: "Logo uploaded and hospital updated successfully!",
|
||||
hospital: updatedHospital,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error handling upload:", error.message);
|
||||
|
||||
// Handle JWT verification errors
|
||||
if (error.name === "JsonWebTokenError") {
|
||||
return res.status(401).json({ error: "Invalid or expired token" });
|
||||
}
|
||||
|
||||
// Handle other unexpected errors
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 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;
|
||||
63
src/routes/pdfRoutes.js
Normal file
63
src/routes/pdfRoutes.js
Normal file
@ -0,0 +1,63 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const axios = require('axios');
|
||||
const FormData = require('form-data'); // Import FormData from the library
|
||||
const fs = require('fs');
|
||||
const db = require("../config/database"); // Database connection
|
||||
|
||||
const router = express.Router();
|
||||
const authMiddleware = require('../middlewares/authMiddleware');
|
||||
|
||||
// Configure Multer for file uploads
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
|
||||
router.post('/process-pdf', upload.single('pdf'), async (req, res) => {
|
||||
try {
|
||||
const filePath = req.file.path;
|
||||
const docId = req.body.doc_id;
|
||||
|
||||
// Create a new FormData instance
|
||||
const formData = new FormData();
|
||||
formData.append('pdf', fs.createReadStream(filePath)); // Stream the uploaded file
|
||||
formData.append('doc_id', docId);
|
||||
|
||||
// Send the file and doc_id to the Python API
|
||||
const response = await axios.post('http://127.0.0.1:5000/process-pdf', formData, {
|
||||
headers: formData.getHeaders(), // Proper headers for multipart/form-data
|
||||
});
|
||||
|
||||
// Cleanup the uploaded file
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
res.status(200).json(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error processing PDF:', error.message);
|
||||
res.status(500).json({ error: 'Failed to process PDF' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get(
|
||||
'/get-qa',
|
||||
// authMiddleware.authenticateToken, // Middleware to validate the access token
|
||||
async (req, res) => {
|
||||
try {
|
||||
// Query the database to get data from the questions_answers table
|
||||
const query = 'SELECT * FROM questions_answers';
|
||||
const result = await db.query(query);
|
||||
|
||||
// Return the data as a JSON response
|
||||
res.json({
|
||||
message: 'Got QA data',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching QA data:', error);
|
||||
res.status(500).json({ error: 'Failed to retrieve data' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
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;
|
||||
79
src/routes/users.js
Normal file
79
src/routes/users.js
Normal file
@ -0,0 +1,79 @@
|
||||
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', 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;
|
||||
569
src/schema
Normal file
569
src/schema
Normal file
@ -0,0 +1,569 @@
|
||||
-- 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`)
|
||||
-- CONSTRAINT `interaction_logs_ibfk_1` FOREIGN KEY (`session_id`) REFERENCES `interaction_sessions` (`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 `interaction_sessions`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `interaction_sessions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */
|
||||
;
|
||||
/*!50503 SET character_set_client = utf8mb4 */
|
||||
;
|
||||
CREATE TABLE `interaction_sessions` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`app_user_id` int DEFAULT NULL,
|
||||
`session_type` enum('Chat', 'Query') DEFAULT 'Chat',
|
||||
`started_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`ended_at` timestamp NULL DEFAULT NULL,
|
||||
`hospital_code` varchar(12) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `app_user_id` (`app_user_id`)
|
||||
-- CONSTRAINT `interaction_sessions_ibfk_1` FOREIGN KEY (`app_user_id`) REFERENCES `app_users` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */
|
||||
;
|
||||
--
|
||||
-- Dumping data for table `interaction_sessions`
|
||||
--
|
||||
|
||||
LOCK TABLES `interaction_sessions` WRITE;
|
||||
/*!40000 ALTER TABLE `interaction_sessions` DISABLE KEYS */
|
||||
;
|
||||
/*!40000 ALTER TABLE `interaction_sessions` 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`
|
||||
--
|
||||
|
||||
CREATE TABLE `user_sessions` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
role ENUM('hospital_user', 'super_admin', 'spurrin_admin') NOT NULL,
|
||||
status ENUM('loggedin', 'loggedout') NOT NULL DEFAULT 'loggedout',
|
||||
access_token VARCHAR(500) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE `sessions` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
device_info VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
/*!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
|
||||
);
|
||||
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 };
|
||||
|
||||
|
||||
17
src/services/hospitalService.js
Normal file
17
src/services/hospitalService.js
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
|
||||
const db = require('../config/database');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
exports.createHospital = async (data) => {
|
||||
const { name_hospital, subdomain, primary_admin_email, primary_admin_password,primary_color,secondary_color,logo_url } = data;
|
||||
|
||||
const hashedPassword = await bcrypt.hash(primary_admin_password, 10);
|
||||
|
||||
const query = `
|
||||
INSERT INTO hospitals (name_hospital, subdomain, primary_admin_email, primary_admin_password,primary_color,secondary_color,logo_url)
|
||||
VALUES (?, ?, ?, ?,?,?,?)
|
||||
`;
|
||||
|
||||
return db.query(query, [name_hospital, subdomain, primary_admin_email, hashedPassword,primary_color,secondary_color,logo_url]);
|
||||
};
|
||||
300
src/services/nlpqamapper.js
Normal file
300
src/services/nlpqamapper.js
Normal file
@ -0,0 +1,300 @@
|
||||
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(
|
||||
"http://127.0.0.1:5000/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(
|
||||
"http://127.0.0.1:5000/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(
|
||||
"http://127.0.0.1:5000/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,
|
||||
};
|
||||
5
src/services/roleService.js
Normal file
5
src/services/roleService.js
Normal file
@ -0,0 +1,5 @@
|
||||
const roleModel = require('../models/roleModel');
|
||||
|
||||
exports.getAllRoles = async () => {
|
||||
return await roleModel.getAllRoles();
|
||||
};
|
||||
372
src/services/secondaryWebsocket.js
Normal file
372
src/services/secondaryWebsocket.js
Normal file
@ -0,0 +1,372 @@
|
||||
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 = "https://backend.spurrinai.com";
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fs.readFileSync("/home/ubuntu/spurrin-cleaned-node/certificates/fullchain.pem"),
|
||||
key: fs.readFileSync("/home/ubuntu/spurrin-cleaned-node/certificates/privkey.pem")
|
||||
});
|
||||
|
||||
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,
|
||||
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;
|
||||
}
|
||||
|
||||
console.log("Decoded token-----------------:", decoded);
|
||||
|
||||
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 };
|
||||
33
src/services/superAdminService.js
Normal file
33
src/services/superAdminService.js
Normal file
@ -0,0 +1,33 @@
|
||||
// // services/superAdminService.js
|
||||
// const bcrypt = require('bcrypt');
|
||||
// const superAdminModel = require('../models/superAdminModel');
|
||||
|
||||
// const db = require('../config/database');
|
||||
|
||||
// exports.getAllSuperAdmins = async () => {
|
||||
// return await superAdminModel.getAllSuperAdmins();
|
||||
// };
|
||||
|
||||
// exports.addSuperAdmin = async (superAdminData) => {
|
||||
// const { email, password, role_id, refresh_token } = superAdminData;
|
||||
|
||||
// const result = await db.query(
|
||||
// 'INSERT INTO super_admins (email, hash_password, role_id, refresh_token) VALUES (?, ?, ?, ?)',
|
||||
// [email, password, role_id, refresh_token]
|
||||
// );
|
||||
|
||||
// return { id: result.insertId, email, role_id, refresh_token };
|
||||
// };
|
||||
|
||||
// exports.updateRefreshToken = async (id, refreshToken) => {
|
||||
// await db.query('UPDATE super_admins SET refresh_token = ? WHERE id = ?', [refreshToken, id]);
|
||||
// };
|
||||
|
||||
// exports.findSuperAdminByEmail = async (email) => {
|
||||
// const result = await db.query('SELECT * FROM super_admins WHERE email = ?', [email]);
|
||||
// return result[0] || null;
|
||||
// };
|
||||
|
||||
// exports.deleteSuperAdmin = async (id) => {
|
||||
// await superAdminModel.deleteSuperAdmin(id);
|
||||
// };
|
||||
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);
|
||||
// },
|
||||
// };
|
||||
69
src/services/userService.js
Normal file
69
src/services/userService.js
Normal file
@ -0,0 +1,69 @@
|
||||
|
||||
const db = require('../config/database');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
|
||||
exports.updateRefreshToken = async (userId, table, refreshToken) => {
|
||||
if (!['super_admins', 'hospital_users'].includes(table)) {
|
||||
throw new Error('Invalid table name');
|
||||
}
|
||||
|
||||
const query = `UPDATE ${table} SET refresh_token = ? WHERE id = ?`;
|
||||
await db.query(query, [refreshToken, userId]);
|
||||
};
|
||||
|
||||
|
||||
exports.findUserByEmail = async (email) => {
|
||||
const query = `SELECT * FROM hospital_users WHERE email = ?`;
|
||||
const [user] = await db.query(query, [email]);
|
||||
return user || null;
|
||||
};
|
||||
|
||||
|
||||
const 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');
|
||||
};
|
||||
|
||||
exports.addUser = async (data) => {
|
||||
const { role_id, ...rest } = data;
|
||||
|
||||
try {
|
||||
const tableName = resolveTableName(role_id);
|
||||
console.log(`Resolved Table: ${tableName}`);
|
||||
|
||||
const passwordHash = await bcrypt.hash(data.password, 10);
|
||||
console.log('Password Hashed Successfully:', passwordHash);
|
||||
|
||||
const query = `
|
||||
INSERT INTO ${tableName}
|
||||
(hospital_id, email, hash_password, role_id, is_default_admin, requires_onboarding, password_reset_required, profile_photo_url, phone_number, bio, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
console.log('Executing Query:', query);
|
||||
const result = await db.query(query, [
|
||||
rest.hospital_id,
|
||||
rest.email,
|
||||
passwordHash,
|
||||
role_id,
|
||||
rest.is_default_admin,
|
||||
rest.requires_onboarding,
|
||||
rest.password_reset_required,
|
||||
rest.profile_photo_url,
|
||||
rest.phone_number,
|
||||
rest.bio,
|
||||
rest.status,
|
||||
]);
|
||||
|
||||
console.log('User added successfully:', result);
|
||||
return { id: result.insertId, ...rest };
|
||||
} catch (error) {
|
||||
console.error('Error in addUser:', error.message);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Exports from userService:', module.exports);
|
||||
|
||||
module.exports = { resolveTableName };
|
||||
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 - 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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user