dev-backup

This commit is contained in:
Ubuntu 2025-06-09 11:11:52 +05:30
commit 5c511a7548
894 changed files with 1063684 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

43
.env.example Normal file
View 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
View File

@ -0,0 +1,3 @@
/node_modules
/.env
/hospital_data

229
CHANGES.md Normal file
View File

@ -0,0 +1,229 @@
# Changes Log
## [Unreleased]
### Added
- Created comprehensive README.md with project documentation
- Implemented structured error handling system
- Added validation middleware using Joi
- Created standardized response handlers
- Implemented async handler utility
- Added custom error classes
- Created hospital validation schemas
- Updated hospital routes with proper middleware
- Added role-based authorization
- Implemented request validation
- Added structured logging
- Created separate authorization middleware with role-based access control
- Created request validation middleware with Joi schema validation
- Added repository layer for database operations
- Implemented database connection pooling
- Added custom error classes for better error handling
- Improved error handling in service layer
### Changed
- Reorganized project structure into src directory
- Updated hospital controller to use new utilities
- Improved error handling in hospital routes
- Enhanced security with proper authentication
- Standardized API response format
- Improved code organization and readability
- Separated authentication and authorization middleware
- Enhanced validation middleware with better error handling and logging
- Refactored hospital routes for better middleware usage
- Moved logo upload logic to controller
- Updated hospital controller methods to use asyncHandler and standardized responses
- Standardized authentication and authorization across all hospital routes
- Improved error handling in hospital user and color management
- Refactored changePassword method to use asyncHandler and standardized responses
- Reordered hospital routes to prevent conflicts
- Fixed route parameter conflicts
- Moved database operations to repository layer
- Improved error handling with custom error classes
- Enhanced database connection management with connection pooling
### Removed
- Removed unused model file (superAdminModel.js)
- Cleaned up empty directories
- Removed redundant code
- Removed inline route handlers in favor of controller methods
- Removed duplicate hospital list method
- Removed old authentication middleware usage
- Removed redundant token validation in changePassword method
- Removed unused imports from hospital routes
- Removed direct database queries from service layer
### Fixed
- Fixed error handling in hospital controller
- Improved validation error messages
- Enhanced security in authentication flow
- Fixed response format consistency
- Fixed asyncHandler import and usage in hospital controller
- Fixed authorize function import and usage in hospital routes
- Fixed validateRequest middleware implementation
- Fixed validateRequest import in hospital routes
- Fixed missing getAllHospitals method in hospital controller
- Fixed error handling in hospital controller methods
- Fixed inconsistent authentication middleware usage
- Fixed missing controller methods and their implementations
- Fixed undefined route handler in changePassword endpoint
- Fixed route conflicts between /users and /:id endpoints
- Fixed missing changePassword route
- Fixed route ordering to prevent parameter conflicts
- Fixed database connection handling
- Fixed error propagation in service layer
## [0.1.0] - Initial Setup
### Added
- Basic project structure
- Database configuration
- Authentication middleware
- Hospital management endpoints
- File upload functionality
- Email notification system
- User management system
- Password reset functionality
- Interaction logging system
### Security
- Implemented JWT authentication
- Added password hashing
- Implemented role-based access control
- Added input validation
- Implemented secure file uploads
- Added email verification system
### Performance
- Implemented database connection pooling
- Added request compression
- Optimized database queries
- Implemented caching where appropriate
### Documentation
- Added API documentation
- Created setup instructions
- Added security guidelines
- Included contribution guidelines
## Hospital Module Improvements
### Code Structure and Organization
- [x] Created dedicated `HospitalService` class for business logic
- [x] Separated concerns between routes, controller, and service layers
- [x] Improved error handling and validation
- [x] Removed duplicate code
- [x] Added proper input validation
- [x] Organized routes with proper middleware
- [x] Added repository layer for database operations
- [x] Implemented database connection pooling
- [x] Added custom error classes
### Security Enhancements
- [x] Added rate limiting (100 requests per 15 minutes per IP)
- [x] Improved file upload security
- Added file type validation (JPEG, PNG, GIF)
- Set file size limit (5MB)
- Secure file naming
- [x] Added input validation through schemas
- [x] Enhanced error messages
- [x] Implemented proper authentication middleware
- [x] Added authorization checks
### Database Optimization
- [x] Improved query structure
- [x] Added proper error handling for database operations
- [x] Implemented better transaction handling
- [x] Added validation before database operations
- [x] Improved error messages for database operations
- [x] Implemented connection pooling
- [x] Added repository layer for better database abstraction
### Additional Improvements
- [x] Better error handling and logging
- [x] Consistent response formats
- [x] Improved code readability
- [x] Better separation of concerns
- [x] Added proper validation for all inputs
- [x] Improved file upload handling
- [x] Added custom error classes
- [x] Improved error propagation
### Pending Improvements
- [ ] Add query caching for frequently accessed data
- [ ] Add request sanitization
- [ ] Implement proper CORS configuration
- [ ] Add security headers
- [ ] Add API documentation
- [ ] Add unit tests
- [ ] Add integration tests
- [ ] Add API tests
## File Structure Changes
```
src/
├── controllers/
│ └── hospitalController.js # Simplified controller with service usage
├── services/
│ └── hospitalService.js # Business logic layer
├── repositories/
│ └── hospitalRepository.js # Database operations layer
├── routes/
│ └── hospitals.js # Updated with security and validation
├── middlewares/
│ ├── authMiddleware.js # Authentication middleware
│ ├── authorizeMiddleware.js # Authorization middleware
│ └── validateRequest.js # Request validation middleware
├── utils/
│ └── errors.js # Custom error classes
└── validators/
└── hospitalValidator.js # Validation schemas
```
## Security Improvements
1. Rate Limiting
- Added express-rate-limit
- 100 requests per 15 minutes per IP
- Custom error message for rate limit exceeded
2. File Upload Security
- File type validation
- File size limits
- Secure file naming
- Proper error handling
3. Input Validation
- Added validation schemas
- Proper error messages
- Type checking
- Required field validation
4. Authentication & Authorization
- Token-based authentication
- Role-based authorization
- Proper error handling for unauthorized access
## Performance Improvements
1. Database Operations
- Optimized queries
- Better error handling
- Transaction support
- Input validation before database operations
- Connection pooling
- Repository pattern implementation
2. Code Organization
- Service layer for business logic
- Repository layer for database operations
- Controller for request handling
- Routes for endpoint definition
- Middleware for cross-cutting concerns
## Next Steps
1. Implement query caching
2. Add comprehensive testing
3. Add API documentation
4. Enhance security measures
5. Add monitoring and logging
flag added to trigger logout to both websockets (secondary and main)

126
Jenkinsfile vendored Normal file
View 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"
}
}
}

Binary file not shown.

BIN
certificates.zip Normal file

Binary file not shown.

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

File diff suppressed because it is too large Load Diff

2043
chat copy 3.py Normal file

File diff suppressed because it is too large Load Diff

1801
chat copy 4.py Normal file

File diff suppressed because it is too large Load Diff

3857
chat copy 5.py Normal file

File diff suppressed because it is too large Load Diff

2161
chat copy.py Normal file

File diff suppressed because it is too large Load Diff

1621
chat.py Normal file

File diff suppressed because it is too large Load Diff

104
docs/API.md Normal file
View File

@ -0,0 +1,104 @@
# API Documentation
## Authentication
All API endpoints require authentication using JWT tokens.
### Headers
```
Authorization: Bearer <token>
```
## Endpoints
### Authentication
- `POST /api/users/hospital-users/login` - Generates userId, roleId and roleName from given user cridentials
- `GET /api/users/refresh-token/{{user_id}}/{{role_id}}` - Generates refresh token for hospitals and their users with roles namely Admin Superadmin, Spurrinadmin and Viewer
- `POST /api/users/get-access-token` - Generates access token for hospitals and their users with roles namely Admin, Superadmin and Viewer
- `POST /api/auth/refresh` - Generates access token for Spurrinadmin
- `POST /api/auth/login` - Login with token validation and hospital status check (for hospital users)
### Spurrinadmin
- `GET /api/super-admin` - Get all super admins
- `POST /api/super-admin/initialize` - Add new super admin
- `DELETE /api/super-admin/:id` - Delete super admin
### Hospitals
- `POST /api/hospitals/create-hospital` Create hospital
- `PUT /api/hospitals/update/:id` - Update hospital details
- `DELETE /api/hospitals/delete/:id` - Delete hospital
- `GET /api/hospitals/list` - Get list of hospitals
- `GET /api/hispitals/list/:{hospital_id}` - get hospital by id
- `GET /api/hospitals/users` - get list of hospital users
- `GET /api/hospitals/colors` - get colors from hospital
SuperAdmin
- `POST /api/hospitals/send-temp-password` - send temporary password to email
- `POST /api/hospitals/change-password` - change the temporary password
Admin and viewer
- `POST /api/hospitals/send-temp-password-av` - send temporary password to email
- `POST /api/hospitals/change-password-av` - send temporary password
- `POST /api/hospitals/update-admin-name` - update admin name
- `POST /api/hospitals/check-user-notification` - Check new app user notification regarding notification
- `PUT /api/hospitals/update-user-notification/:id` - Update app user notification status to checked (boolean)
- `POST /api/hospitals/interaction-logs` - Get interaction logs of hospital's app users
- `PUT /api/hospitals/public-signup/:id` - Update allow public signup
### Users
- `POST /api/users/add-user` - add new user to hospital
- `PUT /api/users/edit-user/:id` - edit hospital user
- `delete /api/users/add-user` - delete hospital user
- `POST /api/upload-profile-photo` - upload profile photo
- `PUT /api/users/update-password/:id` - update password of user
- `POST /api/users/get-spu-access-token` - Get SpurrinAdmin access token
- `POST /api/users/hospital-users/login` - Get hospital user ID
- `POST /api/users/logout` - User logout
- `GET /api/users/refresh-token/:user_id/:role_id` - Get refresh token by user ID
### App Users
- `POST /api/app-users/signup` - App user registration
- `POST /api/app-users/login` - App user login
- `PUT /api/app-users/hitlike` - Like interaction
- `PUT /api/app-users/query-title` - Update query title
- `DELETE /api/app-users/query-title` - Delete query title
- `PUT /api/app-users/like-session` - Like session
- `PUT /api/app-users/approve-user/:appUserId` - Approve app user
- `DELETE /api/app-users/:userId` - Delete app user
### Documents
- `PUT /api/documents/update-status/:id` - Update document status
- `DELETE /api/documents/delete/:id` - Delete document
### Feedback
- `POST /api/feedbacks/app-user/submit` - Submit app user feedback
### Analytics
- `POST /api/analytics/hospitals/active` - Get active hospitals analysis
### Excel Data
- `POST /api/excel-data` - Upload bulk users
### System
- `GET /health` - Health check endpoint
- `POST /api/sync-database` - Database synchronization (development only)
- `GET /` - Root endpoint
## Role-Based Access Control
Some endpoints require specific roles:
- Spurrinadmin - Role ID 6
- Superadmin - Role ID 7
- Admin - Role ID 8
- Viewer - Role ID 9
## File Upload
- Supported file types: Images, documents like pdf
- Upload directory: `/uploads/id_photos/`
`/uploads/documents/`
`/uploads/profile_photos`

23743
error.log Normal file

File diff suppressed because it is too large Load Diff

64
logs/access.log Normal file
View 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

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

File diff suppressed because it is too large Load Diff

21518
logs/combined.log Normal file

File diff suppressed because one or more lines are too long

317
logs/error.log Normal file
View 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
View File

139
model_manager.py Normal file
View File

@ -0,0 +1,139 @@
from transformers import AutoTokenizer, AutoModel
from sentence_transformers import SentenceTransformer
import torch
from sklearn.metrics.pairwise import cosine_similarity
import logging
import atexit
class ModelManager:
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super(ModelManager, cls).__new__(cls)
return cls._instance
def __init__(self):
if not ModelManager._initialized:
logging.info("Initializing ModelManager - Loading models...")
self.load_models()
ModelManager._initialized = True
atexit.register(self.cleanup)
def load_models(self):
try:
# Load models with specific device placement
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
logging.info(f"Using device: {self.device}")
# Enable model caching
torch.hub.set_dir('./model_cache')
# Load models with batch preparation
self.sentence_model = SentenceTransformer('all-MiniLM-L6-v2')
self.sentence_model.to(self.device)
self.sentence_model.eval() # Set to evaluation mode
self.bert_tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
self.bert_model = AutoModel.from_pretrained('bert-base-uncased')
self.bert_model.to(self.device)
self.bert_model.eval() # Set to evaluation mode
# Initialize embedding cache with batch support
self.embedding_cache = {}
self.max_cache_size = 10000
self.batch_size = 32 # Optimize batch size
logging.info("Models loaded successfully with batch optimization")
except Exception as e:
logging.error(f"Error loading models: {e}")
raise
def get_bert_embeddings(self, texts):
if isinstance(texts, str):
texts = [texts]
# Process in batches
all_embeddings = []
for i in range(0, len(texts), self.batch_size):
batch_texts = texts[i:i + self.batch_size]
# Check cache for each text in batch
batch_embeddings = []
uncached_texts = []
uncached_indices = []
for idx, text in enumerate(batch_texts):
cache_key = f"bert_{hash(text)}"
if cache_key in self.embedding_cache:
batch_embeddings.append(self.embedding_cache[cache_key])
else:
uncached_texts.append(text)
uncached_indices.append(idx)
if uncached_texts:
inputs = self.bert_tokenizer(uncached_texts, return_tensors="pt", padding=True, truncation=True).to(self.device)
with torch.no_grad():
outputs = self.bert_model(**inputs)
new_embeddings = outputs.last_hidden_state.mean(dim=1)
# Cache new embeddings
for idx, text in enumerate(uncached_texts):
cache_key = f"bert_{hash(text)}"
if len(self.embedding_cache) < self.max_cache_size:
self.embedding_cache[cache_key] = new_embeddings[idx]
batch_embeddings.insert(uncached_indices[idx], new_embeddings[idx])
all_embeddings.extend(batch_embeddings)
return torch.stack(all_embeddings) if len(all_embeddings) > 1 else all_embeddings[0].unsqueeze(0)
def get_semantic_similarity(self, text1, text2):
# Check cache
cache_key = f"sim_{hash(text1)}_{hash(text2)}"
if cache_key in self.embedding_cache:
return self.embedding_cache[cache_key]
# Preprocess texts for better matching
text1 = text1.lower().strip()
text2 = text2.lower().strip()
# Enhanced batch process embeddings with context awareness
with torch.no_grad():
# Sentence transformer similarity with increased weight
emb1 = self.sentence_model.encode([text1], batch_size=1, convert_to_numpy=True)
emb2 = self.sentence_model.encode([text2], batch_size=1, convert_to_numpy=True)
sent_sim = cosine_similarity(emb1, emb2)[0][0]
# BERT similarity for deeper semantic understanding
bert_emb1 = self.get_bert_embeddings(text1).cpu().numpy()
bert_emb2 = self.get_bert_embeddings(text2).cpu().numpy()
bert_sim = cosine_similarity(bert_emb1, bert_emb2)[0][0]
# Adjusted weights for better follow-up detection
similarity = 0.8 * sent_sim + 0.2 * bert_sim
# Boost similarity for related context
if any(word in text2.split() for word in text1.split()):
similarity = min(1.0, similarity * 1.2)
# Cache the result
if len(self.embedding_cache) < self.max_cache_size:
self.embedding_cache[cache_key] = similarity
return similarity
def cleanup(self):
"""Cleanup models and free memory"""
logging.info("Cleaning up models...")
try:
del self.sentence_model
del self.bert_model
del self.bert_tokenizer
torch.cuda.empty_cache() if torch.cuda.is_available() else None
self.embedding_cache.clear()
logging.info("Models cleaned up successfully")
except Exception as e:
logging.error(f"Error during cleanup: {e}")

6
nodemon.json Normal file
View File

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

8257
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
package.json Normal file
View File

@ -0,0 +1,65 @@
{
"name": "spurrinai-backend",
"version": "1.0.0",
"description": "SpurrinAI Backend Node.js Application",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "npm run test:unit && npm run test:integration",
"test:unit": "jest tests/unit",
"test:integration": "jest tests/integration",
"setup": "node scripts/setup.js",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write \"src/**/*.js\"",
"migrate": "node src/migrations/runMigrations.js",
"migrate:up": "node src/migrations/runMigrations.js up",
"migrate:down": "node src/migrations/runMigrations.js down",
"migrate:create": "node src/migrations/createMigration.js"
},
"keywords": [],
"author": "Tech4biz Solutions",
"license": "UNLICENSED",
"private": true,
"dependencies": {
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"compression": "^1.7.4",
"compromise": "^14.14.4",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"fast-levenshtein": "^3.0.0",
"form-data": "^4.0.1",
"helmet": "^7.0.0",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"mysql": "^2.18.1",
"mysql2": "^3.2.0",
"natural": "^8.0.1",
"node-cron": "^3.0.3",
"nodemailer": "^6.10.0",
"number-to-words": "^1.2.4",
"path": "^0.12.7",
"socket.io": "^4.8.1",
"stopword": "^3.1.4",
"string-similarity": "^4.0.4",
"uuid": "^11.0.5",
"winston": "^3.17.0",
"ws": "^8.13.0"
},
"devDependencies": {
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0",
"nodemon": "^2.0.22",
"prettier": "^2.8.7"
},
"engines": {
"node": ">=14.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

200
readme.md Normal file
View File

@ -0,0 +1,200 @@
# SpurrinAI Backend
A Node.js backend application for SpurrinAI platform.
## Project Structure
```
project-root/
├── src/ # Source code
│ ├── app.js # App entry point
│ ├── config/ # Configuration files
│ ├── controllers/ # Route controllers
│ ├── middleware/ # Custom middleware
│ ├── migrations/ # Database migrations
│ ├── routes/ # Route definitions
│ ├── services/ # Business logic
│ └── utils/ # Utility functions
├── docs/ # Documentation
├── logs/ # Application logs
├── scripts/ # Build and setup scripts
├── tests/ # Test files
└── uploads/ # User uploads
```
## Prerequisites
- Node.js >= 14.0.0
- MySQL >= 5.7
- npm >= 6.0.0
## Installation
1. Clone the repository:
```bash
git clone -b dev https://git.tech4biz.wiki/Tech4Biz-Services/spurrin-cleaned-node.git
cd spurrinai-backend
```
2. Install dependencies:
```bash
npm install
```
3. Set up environment variables:
```bash
cp .env.example .env
# Edit .env with your configuration
```
4. Run the setup script:
```bash
npm run setup
```
## Development Mode
1. Start the development server with hot-reload:
```bash
npm run dev
```
2. Run tests:
```bash
# Run all tests
npm test
# Run unit tests only
npm run test:unit
# Run integration tests only
npm run test:integration
```
3. Code Quality:
```bash
# Lint code
npm run lint
# Fix linting issues
npm run lint:fix
# Format code
npm run format
```
## Production Mode
1. Build the application:
```bash
npm run build
```
2. Start the production server:
```bash
npm start
```
3. For production deployment, ensure:
- Set `NODE_ENV=production` in `.env`
- Configure proper database credentials
- Set up SSL/TLS certificates
- Configure proper logging
- Set up process manager (PM2 recommended)
### Using PM2 (Recommended for Production)
1. Install PM2 globally:
```bash
npm install -g pm2
```
2. Start the application with PM2:
```bash
pm2 start src/app.js --name spurrinai-backend
```
3. Other useful PM2 commands:
```bash
# Monitor application
pm2 monit
# View logs
pm2 logs spurrinai-backend
# Restart application
pm2 restart spurrinai-backend
# Stop application
pm2 stop spurrinai-backend
# Flush logs
pm2 flush
# Delete all logs
pm2 flush spurrinai-backend
# Reload application with zero downtime
pm2 reload spurrinai-backend
```
## Database Migrations
```bash
# Create new migration
npm run migrate:create
# Run all migrations
npm run migrate
# Run migrations up
npm run migrate:up
# Run migrations down
npm run migrate:down
```
## API Documentation
Detailed API documentation can be found in the [docs/API.md](docs/API.md) file.
## Environment Variables
Required environment variables in `.env`:
```env
# Server Configuration
PORT=3000
NODE_ENV=development
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=spurrinai
DB_USER=postgres
DB_PASSWORD=your_password
# JWT Configuration
JWT_SECRET=your_jwt_secret
JWT_EXPIRES_IN=24h
# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password
# File Upload Configuration
UPLOAD_DIR=uploads
MAX_FILE_SIZE=5242880 # 5MB
```
## Support
For support, please contact:
- Email: contact@tech4biz.io
- Issue Tracker: GitHub Issues
## License
UNLICENSED - All rights reserved by Tech4biz Solutions

32
requirements.txt Normal file
View File

@ -0,0 +1,32 @@
# Standard library packages are not included (e.g., os, sys, threading)
# Environment and utility
python-dotenv
tqdm
# Flask and CORS
Flask
flask-cors
# NLP and embeddings
spacy
nltk
openai
langchain
langchain-community
rapidfuzz
# Redis
redis
# Async MySQL
aiomysql
# Concurrency
asyncio
# Optional: For logging in structured or advanced environments
# (not strictly needed unless using specific logging handlers or formats)
# Ensure spacy language model is installed (run manually post-install)
# python -m spacy download en_core_web_sm

29
scripts/setup.js Normal file
View 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

File diff suppressed because it is too large Load Diff

189
src/app.js Normal file
View 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
View File

@ -0,0 +1,88 @@
require('dotenv').config();
const mysql = require('mysql2/promise');
const config = require('./index');
// Create a connection pool
const pool = mysql.createPool({
host: process.env.DB_HOST || '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
View 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
View 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
View 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
View 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

Binary file not shown.

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

File diff suppressed because it is too large Load Diff

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

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

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

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

File diff suppressed because it is too large Load Diff

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

View File

@ -0,0 +1,10 @@
const roleService = require('../services/roleService');
exports.getAllRoles = async (req, res) => {
try {
const roles = await roleService.getAllRoles();
res.json(roles);
} catch (error) {
res.status(500).json({ error: error.message });
}
};

View File

@ -0,0 +1,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" });
}
};

View 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

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

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

View File

@ -0,0 +1,73 @@
const { AppError } = require('../utils/errors');
const logger = require('../utils/logger');
/**
* Global error handling middleware
*/
const errorHandler = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// Log error
logger.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
ip: req.ip,
user: req.user?.id
});
// Development error response
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
}
// Production error response
else {
// Operational, trusted error: send message to client
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
}
// Programming or other unknown error: don't leak error details
else {
logger.error('UNEXPECTED ERROR 💥', err);
res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
}
}
};
// Handle specific error types
const handleSequelizeError = (err) => {
if (err.name === 'SequelizeValidationError') {
return new AppError(err.message, 400);
}
if (err.name === 'SequelizeUniqueConstraintError') {
return new AppError('Duplicate field value entered', 400);
}
return err;
};
const handleJWTError = () =>
new AppError('Invalid token. Please log in again!', 401);
const handleJWTExpiredError = () =>
new AppError('Your token has expired! Please log in again.', 401);
module.exports = {
AppError,
errorHandler,
handleSequelizeError,
handleJWTError,
handleJWTExpiredError
};

View File

@ -0,0 +1,52 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// select folder to upload the image
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadPath = "uploads/profile_photos/";
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
console.log('Multer filename function - file.originalname:', file.originalname);
const fileExtension = path.extname(file.originalname);
console.log('Multer filename function - fileExtension:', fileExtension);
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, `${file.fieldname}-${uniqueSuffix}${fileExtension}`);
},
});
// choose only image
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only image files are allowed'), false);
}
},
limits: { fileSize: 500 * 1024 * 1024 },
});
const profilePhotoUploadMiddleware = (req, res, next) => {
upload.single('profile_photo')(req, res, function (err) {
if (err instanceof multer.MulterError) {
// A Multer error occurred when uploading.
return res.status(400).json({ error: err.message });
} else if (err) {
// An unknown error occurred when uploading.
return res.status(500).json({ error: err.message });
}
// Everything went fine, proceed to the next middleware or controller
next();
});
};
module.exports = profilePhotoUploadMiddleware;

View File

@ -0,0 +1,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
View File

@ -0,0 +1,114 @@
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const config = require('../config');
const logger = require('../utils/logger');
// Rate limiting configuration
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10000000, // High limit for now; tune for production
message: 'Too many requests from this IP, please try again later.',
handler: (req, res) => {
logger.warn(`Rate limit exceeded: ${req.ip} at ${new Date().toISOString()}`);
res.status(429).json({
status: 'error',
message: 'Too many requests from this IP, please try again later.'
});
}
});
// Security headers configuration
const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "wss:", "https:"],
fontSrc: ["'self'", "https:", "data:"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["*"],
frameAncestors: ["*"]
}
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true,
// ✅ FIX: Allow cross-origin loading of resources like images
crossOriginResourcePolicy: { policy: "cross-origin" },
dnsPrefetchControl: { allow: false },
frameguard: { action: "deny" },
hidePoweredBy: true,
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
ieNoOpen: true,
noSniff: true,
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
xssFilter: true
});
// Request validation middleware
const validateRequest = (req, res, next) => {
if (['POST', 'PUT'].includes(req.method) && !req.is('application/json') && !req.is('multipart/form-data')) {
return res.status(415).json({
status: 'error',
message: 'Unsupported Media Type. Only application/json or multipart/form-data is allowed for POST/PUT requests.'
});
}
next();
};
// CORS configuration
const corsOptions = {
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const allowedOrigins = [
'http://192.168.1.19:8081',
'http://localhost:5173',
'http://localhost:5174',
'https://spurrinai.com',
'https://www.spurrinai.com',
'http://localhost:3000',
'https://www.spurrinai.org',
'https://www.spurrinai.info',
'https://spurrinai.info',
'http://spurrinai.info',
'https://34a4-122-171-20-117.ngrok-free.app',
'http://34a4-122-171-20-117.ngrok-free.app'
];
const isOriginAllowed = (
/^http:\/\/[a-z0-9-]+\.localhost(:\d+)?$/.test(origin) ||
/^https:\/\/[a-z0-9-]+\.spurrinai\.com$/.test(origin) ||
/^https:\/\/[a-z0-9-]+\.spurrinai\.org$/.test(origin) ||
/^https:\/\/[a-z0-9-]+\.spurrinai\.info$/.test(origin) ||
allowedOrigins.includes(origin)
);
if (isOriginAllowed) {
callback(null, true);
} else {
logger.warn(`CORS blocked request from origin: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
exposedHeaders: ['Content-Range', 'X-Content-Range'],
maxAge: 86400
};
module.exports = {
apiLimiter,
securityHeaders,
validateRequest,
corsOptions
};

View File

@ -0,0 +1,34 @@
const { ValidationError } = require('../utils/errors');
const logger = require('../utils/logger');
/**
* Middleware to validate request data against a Joi schema
* @param {Object} schema - Joi validation schema
* @returns {Function} Express middleware function
*/
const validateRequest = (schema) => {
return (req, res, next) => {
try {
const { error } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true,
allowUnknown: false
});
if (error) {
const errorMessage = error.details
.map(detail => detail.message)
.join(', ');
logger.warn(`Validation error: ${errorMessage}`);
throw new ValidationError(errorMessage);
}
next();
} catch (error) {
next(error);
}
};
};
module.exports = validateRequest;

View File

@ -0,0 +1,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();

View File

@ -0,0 +1,157 @@
const db = require('../config/database');
const fs = require('fs').promises;
const path = require('path');
class MigrationRunner {
constructor() {
this.migrationsTable = 'migrations';
}
async initialize() {
try {
// Create migrations table if it doesn't exist
await db.query(`
CREATE TABLE IF NOT EXISTS ${this.migrationsTable} (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
} catch (error) {
console.error('Error initializing migrations table:', error);
throw error;
}
}
async getExecutedMigrations() {
try {
const rows = await db.query(
`SELECT name FROM ${this.migrationsTable} ORDER BY executed_at ASC`
);
// MySQL2 returns [rows, fields] where rows is an array
return Array.isArray(rows) ? rows.map(row => row.name) : [];
} catch (error) {
// If table doesn't exist, return empty array
if (error.code === 'ER_NO_SUCH_TABLE') {
return [];
}
throw error;
}
}
async runMigrations() {
try {
await this.initialize();
const executedMigrations = await this.getExecutedMigrations();
const migrationsDir = path.join(__dirname, 'migrations');
const files = await fs.readdir(migrationsDir);
const migrationFiles = files
.filter(f => f.endsWith('.js'))
.sort();
for (const file of migrationFiles) {
if (!executedMigrations.includes(file)) {
console.log(`Running migration: ${file}`);
const migration = require(path.join(migrationsDir, file));
// Start transaction
await db.query('START TRANSACTION');
try {
await migration.up();
// Use INSERT IGNORE to handle duplicate entries gracefully
await db.query(
`INSERT IGNORE INTO ${this.migrationsTable} (name) VALUES (?)`,
[file]
);
await db.query('COMMIT');
console.log(`Successfully executed migration: ${file}`);
} catch (error) {
await db.query('ROLLBACK');
// If it's a duplicate entry error, just log and continue
if (error.code === 'ER_DUP_ENTRY') {
console.log(`Migration ${file} was already executed, skipping...`);
continue;
}
console.error(`Error executing migration ${file}:`, error);
throw error;
}
} else {
console.log(`Migration ${file} was already executed, skipping...`);
}
}
console.log('All migrations completed successfully');
} catch (error) {
console.error('Migration failed:', error);
throw error;
}
}
async rollback() {
try {
// First check if migrations table exists and has entries
const migrations = await db.query(`
SELECT COUNT(*) as count
FROM ${this.migrationsTable}
`).catch(() => [{ count: 0 }]);
if (migrations[0].count === 0) {
// If no migrations in table, check if any migrations exist in directory
const migrationsDir = path.join(__dirname, 'migrations');
const files = await fs.readdir(migrationsDir);
const migrationFiles = files
.filter(f => f.endsWith('.js'))
.sort();
if (migrationFiles.length === 0) {
console.log('No migrations found to rollback');
return;
}
// If migrations exist but aren't tracked, ask user what to do
console.log('Found migrations in directory but none are tracked in the database.');
console.log('Available migrations:');
migrationFiles.forEach(file => console.log(`- ${file}`));
console.log('\nTo rollback a specific migration, please run:');
console.log('npm run migrate:down -- --migration=<migration_name>');
return;
}
const executedMigrations = await this.getExecutedMigrations();
if (executedMigrations.length === 0) {
console.log('No migrations to rollback');
return;
}
const lastMigration = executedMigrations[executedMigrations.length - 1];
console.log(`Rolling back migration: ${lastMigration}`);
const migration = require(path.join(__dirname, 'migrations', lastMigration));
// Start transaction
await db.query('START TRANSACTION');
try {
await migration.down();
await db.query(
`DELETE FROM ${this.migrationsTable} WHERE name = ?`,
[lastMigration]
);
await db.query('COMMIT');
console.log(`Successfully rolled back migration: ${lastMigration}`);
} catch (error) {
await db.query('ROLLBACK');
console.error(`Error rolling back migration ${lastMigration}:`, error);
throw error;
}
} catch (error) {
console.error('Rollback failed:', error);
throw error;
}
}
}
module.exports = new MigrationRunner();

View File

@ -0,0 +1,45 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Check if column exists
const columns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'interaction_logs'
AND COLUMN_NAME = 'is_liked'
`);
// Only add column if it doesn't exist
if (columns.length === 0) {
await db.query(`
ALTER TABLE interaction_logs
ADD COLUMN is_liked BOOLEAN DEFAULT FALSE AFTER response
`);
console.log('Added is_liked column to interaction_logs table');
} else {
console.log('is_liked column already exists in interaction_logs table');
}
},
async down() {
// Check if column exists before dropping
const columns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'interaction_logs'
AND COLUMN_NAME = 'is_liked'
`);
if (columns.length > 0) {
await db.query(`
ALTER TABLE interaction_logs
DROP COLUMN is_liked
`);
console.log('Dropped is_liked column from interaction_logs table');
} else {
console.log('is_liked column does not exist in interaction_logs table');
}
}
};

View File

@ -0,0 +1,44 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Check if column exists
const columns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospitals'
AND COLUMN_NAME = 'publicSignupEnabled'
`);
// Only add column if it doesn't exist
if (columns.length === 0) {
await db.query(`
ALTER TABLE hospitals
ADD COLUMN publicSignupEnabled BOOLEAN DEFAULT FALSE AFTER status
`);
console.log('Added publicSignupEnabled column to hospitals table');
} else {
console.log('publicSignupEnabled column already exists in hospitals table');
}
},
async down() {
// Check if column exists before dropping
const columns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospitals'
AND COLUMN_NAME = 'publicSignupEnabled'
`);
if (columns.length > 0) {
await db.query(`
ALTER TABLE hospitals
DROP COLUMN publicSignupEnabled
`);
console.log('Dropped publicSignupEnabled column from hospitals table');
} else {
console.log('publicSignupEnabled column does not exist in hospitals table');
}
}
};

View File

@ -0,0 +1,79 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Add temporary_password to hospitals table
const hospitalsColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospitals'
AND COLUMN_NAME = 'temporary_password'
`);
if (hospitalsColumns.length === 0) {
await db.query(`
ALTER TABLE hospitals
ADD COLUMN temporary_password VARCHAR(255) NULL
`);
console.log('Added temporary_password column to hospitals table');
} else {
console.log('temporary_password column already exists in hospitals table');
}
// Add temporary_password to hospital_users table
const hospitalUsersColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospital_users'
AND COLUMN_NAME = 'temporary_password'
`);
if (hospitalUsersColumns.length === 0) {
await db.query(`
ALTER TABLE hospital_users
ADD COLUMN temporary_password VARCHAR(255) NULL
`);
console.log('Added temporary_password column to hospital_users table');
} else {
console.log('temporary_password column already exists in hospital_users table');
}
},
async down() {
// Drop temporary_password from hospitals table
const hospitalsColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospitals'
AND COLUMN_NAME = 'temporary_password'
`);
if (hospitalsColumns.length > 0) {
await db.query(`
ALTER TABLE hospitals
DROP COLUMN temporary_password
`);
console.log('Dropped temporary_password column from hospitals table');
} else {
console.log('temporary_password column does not exist in hospitals table');
}
// Drop temporary_password from hospital_users table
const hospitalUsersColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospital_users'
AND COLUMN_NAME = 'temporary_password'
`);
if (hospitalUsersColumns.length > 0) {
await db.query(`
ALTER TABLE hospital_users
DROP COLUMN temporary_password
`);
console.log('Dropped temporary_password column from hospital_users table');
} else {
console.log('temporary_password column does not exist in hospital_users table');
}
}
};

View File

@ -0,0 +1,77 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Check if column exists and is NOT NULL before altering
const ratingColumn = await db.query(`
SELECT IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'feedback'
AND COLUMN_NAME = 'rating'
`);
const informationReceivedColumn = await db.query(`
SELECT IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'feedback'
AND COLUMN_NAME = 'information_received'
`);
if (ratingColumn.length > 0 && ratingColumn[0].IS_NULLABLE === 'NO') {
await db.query(`
ALTER TABLE feedback
MODIFY COLUMN rating ENUM('Terrible', 'Bad', 'Okay', 'Good', 'Awesome') NULL
`);
console.log('Modified rating column to be nullable in feedback table');
} else {
console.log('rating column is already nullable or does not exist in feedback table');
}
if (informationReceivedColumn.length > 0 && informationReceivedColumn[0].IS_NULLABLE === 'NO') {
await db.query(`
ALTER TABLE feedback
MODIFY COLUMN information_received ENUM('Yes', 'Partially', 'No') NULL
`);
console.log('Modified information_received column to be nullable in feedback table');
} else {
console.log('information_received column is already nullable or does not exist in feedback table');
}
},
async down() {
// Revert columns to NOT NULL if they are currently nullable
const ratingColumn = await db.query(`
SELECT IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'feedback'
AND COLUMN_NAME = 'rating'
`);
const informationReceivedColumn = await db.query(`
SELECT IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'feedback'
AND COLUMN_NAME = 'information_received'
`);
if (ratingColumn.length > 0 && ratingColumn[0].IS_NULLABLE === 'YES') {
await db.query(`
ALTER TABLE feedback
MODIFY COLUMN rating ENUM('Terrible', 'Bad', 'Okay', 'Good', 'Awesome') NOT NULL
`);
console.log('Reverted rating column to NOT NULL in feedback table');
} else {
console.log('rating column is already NOT NULL or does not exist in feedback table');
}
if (informationReceivedColumn.length > 0 && informationReceivedColumn[0].IS_NULLABLE === 'YES') {
await db.query(`
ALTER TABLE feedback
MODIFY COLUMN information_received ENUM('Yes', 'Partially', 'No') NOT NULL
`);
console.log('Reverted information_received column to NOT NULL in feedback table');
} else {
console.log('information_received column is already NOT NULL or does not exist in feedback table');
}
}
};

View File

@ -0,0 +1,67 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Check if columns already exist to avoid duplicate ALTERs
const existingColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'app_users'
AND COLUMN_NAME IN ('pin_otp', 'pin_otp_expiry')
`);
const existingColumnNames = existingColumns.map(col => col.COLUMN_NAME);
if (!existingColumnNames.includes('pin_otp')) {
await db.query(`
ALTER TABLE app_users
ADD COLUMN pin_otp VARCHAR(6)
`);
console.log('Added pin_otp column to app_users table');
} else {
console.log('pin_otp column already exists in app_users table');
}
if (!existingColumnNames.includes('pin_otp_expiry')) {
await db.query(`
ALTER TABLE app_users
ADD COLUMN pin_otp_expiry DATETIME
`);
console.log('Added pin_otp_expiry column to app_users table');
} else {
console.log('pin_otp_expiry column already exists in app_users table');
}
},
async down() {
// Drop the columns only if they exist
const existingColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'app_users'
AND COLUMN_NAME IN ('pin_otp', 'pin_otp_expiry')
`);
const existingColumnNames = existingColumns.map(col => col.COLUMN_NAME);
if (existingColumnNames.includes('pin_otp')) {
await db.query(`
ALTER TABLE app_users
DROP COLUMN pin_otp
`);
console.log('Removed pin_otp column from app_users table');
} else {
console.log('pin_otp column does not exist in app_users table');
}
if (existingColumnNames.includes('pin_otp_expiry')) {
await db.query(`
ALTER TABLE app_users
DROP COLUMN pin_otp_expiry
`);
console.log('Removed pin_otp_expiry column from app_users table');
} else {
console.log('pin_otp_expiry column does not exist in app_users table');
}
}
};

View File

@ -0,0 +1,44 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Check if the 'city' column already exists in the 'hospital_users' table
const existingColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospital_users'
AND COLUMN_NAME = 'city'
`);
if (existingColumns.length === 0) {
// Add the 'city' column if it doesn't exist
await db.query(`
ALTER TABLE hospital_users
ADD COLUMN city VARCHAR(225)
`);
console.log('Added city column to hospital_users table');
} else {
console.log('city column already exists in hospital_users table');
}
},
async down() {
// Drop the 'city' column if it exists
const existingColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'hospital_users'
AND COLUMN_NAME = 'city'
`);
if (existingColumns.length > 0) {
await db.query(`
ALTER TABLE hospital_users
DROP COLUMN city
`);
console.log('Removed city column from hospital_users table');
} else {
console.log('city column does not exist in hospital_users table');
}
}
};

View File

@ -0,0 +1,47 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Check if the column already exists
const existingColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'feedback'
AND COLUMN_NAME = 'is_forwarded'
`);
const columnExists = existingColumns.length > 0;
if (!columnExists) {
await db.query(`
ALTER TABLE feedback
ADD COLUMN is_forwarded BOOLEAN DEFAULT 0
`);
console.log('✅ Added is_forwarded column to feedback table');
} else {
console.log('⚠️ is_forwarded column already exists in feedback table');
}
},
async down() {
// Check again before dropping
const existingColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'feedback'
AND COLUMN_NAME = 'is_forwarded'
`);
const columnExists = existingColumns.length > 0;
if (columnExists) {
await db.query(`
ALTER TABLE feedback
DROP COLUMN is_forwarded
`);
console.log('🗑️ Removed is_forwarded column from feedback table');
} else {
console.log('⚠️ is_forwarded column does not exist in feedback table');
}
}
};

View File

@ -0,0 +1,43 @@
const db = require('../../config/database');
module.exports = {
async up() {
// Add temporary_password to super_admins table
const superAdminColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'super_admins'
AND COLUMN_NAME = 'temporary_password'
`);
if (superAdminColumns.length === 0) {
await db.query(`
ALTER TABLE super_admins
ADD COLUMN temporary_password VARCHAR(255) NULL
`);
console.log('✅ Added temporary_password column to super_admins table');
} else {
console.log('⚠️ temporary_password column already exists in super_admins table');
}
},
async down() {
// Drop temporary_password from super_admins table
const superAdminColumns = await db.query(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'super_admins'
AND COLUMN_NAME = 'temporary_password'
`);
if (superAdminColumns.length > 0) {
await db.query(`
ALTER TABLE super_admins
DROP COLUMN temporary_password
`);
console.log('🗑️ Dropped temporary_password column from super_admins table');
} else {
console.log('⚠️ temporary_password column does not exist in super_admins table');
}
}
};

View File

@ -0,0 +1,26 @@
const migrationRunner = require('./migrationRunner');
async function run() {
try {
const command = process.argv[2];
switch (command) {
case 'up':
await migrationRunner.runMigrations();
break;
case 'down':
await migrationRunner.rollback();
break;
default:
console.log('Usage: node runMigrations.js [up|down]');
process.exit(1);
}
process.exit(0);
} catch (error) {
console.error('Migration error:', error);
process.exit(1);
}
}
run();

View File

@ -0,0 +1,37 @@
const db = require('../config/database');
/**
* Migration template
*
* To create a new migration:
* 1. Copy this file
* 2. Rename it with timestamp and description (e.g., 20240315000000_create_hospitals_table.js)
* 3. Implement up() and down() methods
* 4. Add your SQL queries
*/
module.exports = {
/**
* Run the migration
*/
async up() {
// Add your migration SQL here
// Example:
// await db.query(`
// CREATE TABLE IF NOT EXISTS table_name (
// id INT AUTO_INCREMENT PRIMARY KEY,
// name VARCHAR(255) NOT NULL,
// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
// )
// `);
},
/**
* Rollback the migration
*/
async down() {
// Add your rollback SQL here
// Example:
// await db.query('DROP TABLE IF EXISTS table_name');
}
};

View 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
View File

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

View File

@ -0,0 +1,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
View File

@ -0,0 +1,14 @@
const express = require('express');
const router = express.Router();
const analysisController = require('../controllers/analysisController');
const { authenticateToken } = require('../middlewares/authMiddleware');
const multer = require('multer');
const upload = multer();
// Analysis routes
router.get('/hospitals/onboarded',upload.none(), authenticateToken, analysisController.getOnboardedHospitalsAnalysis);
router.post('/hospitals/active',upload.none(), authenticateToken, analysisController.getActiveHospitalsAnalysis);
router.get('/users/active',upload.none(), authenticateToken, analysisController.getActiveChatUsersAnalysis);
router.post('/hospitals/registered-users', upload.none(), authenticateToken, analysisController.getHospitalRegisteredUsers);
router.post('/hospitals/active-app-users', upload.none(), authenticateToken, analysisController.getHospitalActiveUsers);
module.exports = router;

164
src/routes/appUsers.js Normal file
View 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
View File

@ -0,0 +1,16 @@
const express = require('express');
const authController = require('../controllers/authController');
const authMiddleware = require('../middlewares/authMiddleware');
const router = express.Router();
// common api endpoint for login
router.post('/login',authMiddleware.authenticateToken,authMiddleware.authenticateOverHospitalStatus, authController.login);
router.post('/refresh', authController.refreshToken);
router.post('/logout', authController.logout);
router.post('/check-token',authController.checkAccessToken)
module.exports = router;

70
src/routes/documents.js Normal file
View 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
View File

@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
const excelController = require('../controllers/exceldataController');
const authMiddleware = require('../middlewares/authMiddleware');
const multer = require('multer');
const upload = multer();
router.post('/',authMiddleware.authenticateToken,upload.none(), excelController.createExcelEntry);
// put and delete will be done from hospital_users route as excel data is nothing but adding hospital_users through excel
module.exports = router;

23
src/routes/feedbacks.js Normal file
View 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
View 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
View File

@ -0,0 +1,16 @@
const express = require('express');
const authMiddleware = require('../middlewares/authMiddleware');
const onboardingController = require('../controllers/onboardingController');
const router = express.Router();
// Route to fetch all onboarding steps for a user
router.get('/:userId', authMiddleware.authenticateToken, onboardingController.getOnboardingSteps);
// Route to add a new onboarding step
router.post('/add', authMiddleware.authenticateToken, onboardingController.addOnboardingStep);
// Route to update an onboarding step
router.put('/update/:user_id', authMiddleware.authenticateToken, onboardingController.updateOnboardingStep);
module.exports = router;

63
src/routes/pdfRoutes.js Normal file
View 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
View File

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

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

@ -0,0 +1,61 @@
const express = require('express');
const superAdminController = require('../controllers/superAdminController');
const authMiddleware = require('../middlewares/authMiddleware');
const router = express.Router();
const multer = require("multer");
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadPath = "uploads/logos/";
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
const fileExtension = file.originalname.split(".").pop(); // Get the file extension
cb(null, `${file.fieldname}-${uniqueSuffix}.${fileExtension}`); // Append the extension
},
});
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith("image/")) {
cb(null, true);
} else {
cb(new Error("Only image files are allowed"), false);
}
},
});
// Route to create Spurrin SuperAdmin without authentication
router.post('/initialize', superAdminController.initializeSuperAdmin);
router.get('/',authMiddleware.authenticateToken ,superAdminController.getAllSuperAdmins);
router.post('/',authMiddleware.authenticateToken ,superAdminController.addSuperAdmin);
router.delete('/:id',authMiddleware.authenticateToken, superAdminController.deleteSuperAdmin);
router.get(
'/data-consumption-report',
authMiddleware.authenticateToken,
superAdminController.getOnboardedHospitals
);
// change password
router.post(
"/send-temp-password",
upload.none(),
superAdminController.sendTempPassword
);
router.post(
"/change-password",
upload.none(),
superAdminController.changeTempPassword
);
module.exports = router;

79
src/routes/users.js Normal file
View 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
View 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
View File

@ -0,0 +1,167 @@
// const cron = require('node-cron');
// const jwt = require('jsonwebtoken');
// const db = require('../config/database'); // Database connection
// // Generate a new refresh token
// const generateRefreshToken = (id, email, role) => {
// const generateRefreshToken = (id, email, role_id) => {
// // Map role_id to role name (e.g., Spurrinadmin, Superadmin)
// const roleMap = {
// 6: 'Spurrinadmin',
// 7: 'Superadmin',
// 8: 'Admin', // Adjust as needed
// 9: 'Viewer',
// };
// const role = roleMap[role_id] || 'UnknownRole';
// return jwt.sign({ id, email, role }, process.env.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
// };
// return jwt.sign({ id, email, role }, process.env.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
// };
// // Function to update expired refresh tokens
// const refreshExpiredTokens = async () => {
// try {
// console.log("🔄 Running Refresh Token Renewal Cron Job...");
// // Check both `super_admins` and `hospital_users` for expiring refresh tokens
// const tables = ['super_admins', 'hospital_users'];
// for (const table of tables) {
// const query = `SELECT id, email, role_id, refresh_token FROM ${table} WHERE refresh_token IS NOT NULL`;
// const result = await db.query(query);
// // **Fix: Ensure the query result is properly formatted**
// let users = [];
// if (Array.isArray(result)) {
// users = result.length > 0 ? result : [];
// } else if (Array.isArray(result[0])) {
// users = result[0]; // Handle case where MySQL returns nested array
// } else {
// console.error(`❌ Unexpected query result format for ${table}:`, result);
// continue; // Skip to next table if unexpected format
// }
// for (const user of users) {
// try {
// jwt.verify(user.refresh_token, process.env.JWT_REFRESH_TOKEN_SECRET);
// } catch (err) {
// if (err.name === 'TokenExpiredError') {
// console.log(`🔄 Refresh Token Expired for User ID: ${user.id} in ${table}`);
// // Generate a new refresh token
// const newRefreshToken = generateRefreshToken(user.id, user.email, user.role_id);
// // Update the database with the new refresh token
// const updateQuery = `UPDATE ${table} SET refresh_token = ? WHERE id = ?`;
// await db.query(updateQuery, [newRefreshToken, user.id]);
// console.log(`✅ New Refresh Token Generated for User ID: ${user.id} in ${table}`);
// }
// }
// }
// }
// } catch (error) {
// console.error("❌ Error refreshing expired tokens:", error.message);
// }
// };
// // Schedule the task to run every 1 hour
// cron.schedule('0 * * * *', async () => {
// await refreshExpiredTokens();
// console.log("🔄 Refresh Token Cron Job Executed Successfully!");
// });
// module.exports = { refreshExpiredTokens };
const cron = require('node-cron');
const jwt = require('jsonwebtoken');
const db = require('../config/database'); // Database connection
// Generate a new refresh token
const generateRefreshToken = (id, email, role_id) => {
const roleMap = {
6: 'Spurrinadmin',
7: 'Superadmin',
8: 'Admin',
9: 'Viewer',
};
const role = roleMap[role_id] || 'UnknownRole';
console.log("role-----",role)
return jwt.sign(
{ id, email, role },
process.env.JWT_REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' } // You can change to '30d' if needed
);
};
// Function to update expired or near-expiry refresh tokens
const refreshExpiredTokens = async () => {
try {
console.log("🔄 Running Refresh Token Renewal Cron Job...");
const tables = ['super_admins', 'hospital_users'];
for (const table of tables) {
try {
const query = `SELECT id, email, role_id, refresh_token FROM ${table} WHERE refresh_token IS NOT NULL`;
const result = await db.query(query);
let users = Array.isArray(result) ? result : Array.isArray(result[0]) ? result[0] : [];
if (users.length === 0) {
console.log(`⚠️ No refresh tokens found in ${table}.`);
continue;
}
for (const user of users) {
try {
const decoded = jwt.verify(user.refresh_token, process.env.JWT_REFRESH_TOKEN_SECRET, { ignoreExpiration: true });
const currentTime = Math.floor(Date.now() / 1000);
const timeToExpire = decoded.exp - currentTime;
if (timeToExpire < 3600) { // Less than 1 hour remaining
console.log(`🔄 Refresh Token Near Expiry for User ID: ${user.id} in ${table}`);
const newRefreshToken = generateRefreshToken(user.id, user.email, user.role_id);
const updateQuery = `UPDATE ${table} SET refresh_token = ? WHERE id = ?`;
await db.query(updateQuery, [newRefreshToken, user.id]);
console.log(`✅ New Refresh Token Generated for User ID: ${user.id} in ${table}, Expires At: ${new Date((decoded.exp + 7 * 24 * 60 * 60) * 1000).toISOString()}`);
}
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.log(`🔴 Refresh Token Expired for User ID: ${user.id} in ${table}`);
const newRefreshToken = generateRefreshToken(user.id, user.email, user.role_id);
const updateQuery = `UPDATE ${table} SET refresh_token = ? WHERE id = ?`;
await db.query(updateQuery, [newRefreshToken, user.id]);
console.log(`✅ New Refresh Token Generated for Expired Token - User ID: ${user.id} in ${table}`);
} else {
console.error(`❌ Invalid Refresh Token for User ID: ${user.id} in ${table}:`, err.message);
}
}
}
} catch (queryError) {
console.error(`❌ Failed to process table ${table}:`, queryError.message);
}
}
} catch (error) {
console.error("❌ Unexpected error in refresh token cron job:", error.message);
}
};
// Schedule the task to run every hour
cron.schedule('0 * * * *', async () => {
await refreshExpiredTokens();
console.log("🔄 Refresh Token Cron Job Executed Successfully!");
});
module.exports = { refreshExpiredTokens };

View File

@ -0,0 +1,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
View 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,
};

View File

@ -0,0 +1,5 @@
const roleModel = require('../models/roleModel');
exports.getAllRoles = async () => {
return await roleModel.getAllRoles();
};

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

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

View File

@ -0,0 +1,42 @@
const jwt = require('jsonwebtoken');
exports.generateAccessToken = (user) => {
return jwt.sign(user, process.env.JWT_ACCESS_TOKEN_SECRET, { expiresIn: '5h' });
};
exports.generateRefreshToken = (user) => {
return jwt.sign(user, process.env.JWT_REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
};
exports.verifyAccessToken = (token) => {
return jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);
};
exports.verifyRefreshToken = (token) => {
return jwt.verify(token, process.env.JWT_REFRESH_TOKEN_SECRET);
};
exports.verifyToken = (token, secret) => {
return jwt.verify(token, secret);
};
// const jwt = require('jsonwebtoken');
// module.exports = {
// generateAccessToken: (payload) => {
// return jwt.sign(payload, process.env.JWT_ACCESS_TOKEN_SECRET, {
// expiresIn: process.env.JWT_ACCESS_TOKEN_EXPIRY || '15m',
// });
// },
// generateRefreshToken: (payload) => {
// return jwt.sign(payload, process.env.JWT_REFRESH_TOKEN_SECRET, {
// expiresIn: process.env.JWT_REFRESH_TOKEN_EXPIRY || '7d',
// });
// },
// verifyToken: (token, secret) => {
// return jwt.verify(token, secret);
// },
// };

View 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
View File

@ -0,0 +1,308 @@
const jwt = require("jsonwebtoken");
const db = require("../config/database"); // Database connection
const WebSocket = require("ws");
const { getAnswerFromQuestion } = require("./nlpqamapper"); // Import the NLP processing module
// Create WebSocket server
const wss = new WebSocket.Server({ port: 40510, perMessageDeflate: false });
// Map to store user connections - key: userId, value: connection object
const userConnections = new Map();
// Map to store user session states - key: userId, value: session data
const userSessionStates = new Map();
console.log("WebSocket Server running on ws://0.0.0.0:40510");
// Log active connections periodically to monitor server health
setInterval(() => {
console.log(`Active WebSocket connections: ${wss.clients.size}`);
console.log(`Mapped user connections: ${userConnections.size}`);
}, 60000);
// Helper function to clean up when a connection is closed
function cleanupConnection(userId) {
if (userId && userConnections.has(userId)) {
console.log(`Cleaning up connection for user: ${userId}`);
userConnections.delete(userId);
userSessionStates.delete(userId);
}
}
wss.on("connection", function (ws, req) {
const origin = req.headers.origin || "Unknown Origin";
console.log(`New client connected from: ${origin}`);
// Set a unique connection ID for logging
ws.connectionId = Date.now() + Math.random().toString(36).substring(2, 10);
console.log(`Assigned connection ID: ${ws.connectionId}`);
// Set connection metadata
ws.isAuthenticated = false;
ws.userId = null;
// Send welcome message
ws.send(JSON.stringify({ message: "Welcome to WebSocket Server!" }));
ws.on("message", async function (message) {
try {
const connectionLog = `[Conn: ${ws.connectionId}]`;
console.log(`${connectionLog} Received message`);
const parsedMessage = JSON.parse(message);
const { query, token, session_id, session_title } = parsedMessage;
// Ensure session_id is present and unique per session
let user_session_id = session_id;
if (!user_session_id) {
// Generate a new session_id if missing (UUID v4)
user_session_id = require('crypto').randomUUID();
console.warn(`${connectionLog} No session_id provided. Generated new session_id: ${user_session_id}`);
}
console.log(`${connectionLog} Using session_id: ${user_session_id}`);
console.log(`${connectionLog} Processing query, timestamp: ${new Date().toISOString()}`);
// Validate basic requirements
if (!token || !query) {
console.error(`${connectionLog} Missing token or query:`, { token: !!token, query: !!query });
ws.send(JSON.stringify({ error: "Token and query are required fields" }));
return;
}
// Validate token
let userData;
let user_hospital_code;
try {
// Verify JWT
userData = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);
// Verify access token in database
const tokenQuery = `SELECT access_token, hospital_code FROM app_users WHERE id = ?`;
const result = await db.query(tokenQuery, [userData.id]);
user_hospital_code = result[0].hospital_code
if (!result.length || token !== result[0].access_token) {
console.error(`${connectionLog} Token mismatch for user: ${userData.id}`);
ws.send(JSON.stringify({ error: "Invalid or mismatched access token" }));
return;
}
// Set authenticated state
ws.isAuthenticated = true;
ws.userId = userData.id;
// Register this connection in the users map
userConnections.set(userData.id, ws);
console.log(`${connectionLog} Authenticated user: ${userData.id}`);
} catch (err) {
console.error(`${connectionLog} Token verification failed:`, err.message);
ws.send(JSON.stringify({ error: "Invalid or expired token" }));
return;
}
const userId = userData.id;
let userQuery
// Validate user is active
const hsptQuery = `SELECT * FROM hospitals WHERE hospital_code = ?`;
const hospitalData = await db.query(hsptQuery, [user_hospital_code]);
if(hospitalData[0].publicSignupEnabled){
userQuery = `
SELECT id, hospital_code, status
FROM app_users
WHERE id = ?
`;
}
else{
// Validate user is active
userQuery = `
SELECT id, hospital_code, status
FROM app_users
WHERE id = ? AND status = 'Active'
`;
}
const userResult = await db.query(userQuery, [userId]);
if (userResult.length === 0) {
console.error(`${connectionLog} Unauthorized or inactive user: ${userId}`);
ws.send(JSON.stringify({ error: "Unauthorized or inactive user" }));
return;
}
const hospital_code = userResult[0].hospital_code;
console.log(`${connectionLog} User ${userId} is active and belongs to hospital: ${hospital_code}`);
// Get or initialize user session state
if (!userSessionStates.has(userId)) {
userSessionStates.set(userId, {
awaitingConfirmation: false,
lastOriginalQuery: '',
activeSessionId: null
});
}
// Get the user-specific session state
const userState = userSessionStates.get(userId);
const receivedQuestion = query.toString();
console.log(`${connectionLog} Received question from user ${userId}: ${receivedQuestion}`);
// Fetch last 5 Q&A pairs for this user and session_id
let context = [];
try {
const contextQuery = `
SELECT query, response
FROM interaction_logs
WHERE app_user_id = ? AND session_id = ?
ORDER BY id DESC
LIMIT 5
`;
const contextResult = await db.query(contextQuery, [userId, user_session_id]);
// Reverse to get chronological order
context = contextResult.reverse();
console.log(`${connectionLog} Context for user ${userId}, session ${user_session_id}:`, context);
} catch (contextErr) {
console.error(`${connectionLog} Error fetching context Q&A:`, contextErr);
}
// Update the user's state with the active session BEFORE calling NLP
userState.activeSessionId = user_session_id;
userState.session_id = user_session_id; // Add for clarity
// Process the query through NLP - pass context
console.log(`${connectionLog} Python API called at: ${new Date().toISOString()}`);
let response;
try {
response = await getAnswerFromQuestion(
receivedQuestion,
hospital_code,
userState,
context // Pass context as argument
);
function getErrorCode(response) {
const match = response.match(/Error code: (\d+)/);
return match ? match[1] : "Unknown Error";
}
let responseStatus = getErrorCode(response);
console.log("response error code----", getErrorCode(response));
console.log("response status----", responseStatus);
if (responseStatus == 400) {
response = "We couldn't understand that request. Please check and try again.";
} else if (responseStatus == 401) {
response = "Session expired. Please log in and try again.";
} else if (responseStatus == 403) {
response = "You don't have permission to access this feature.";
} else if (responseStatus == 404) {
response = "Requested resource not found.";
} else if (responseStatus == 429) {
response = "We're handling a lot right now. Please wait a moment and try again.";
} else if (responseStatus == 500) {
response = "Something went wrong on our end. We're on it!";
} else if (responseStatus == 502 || responseStatus == 503 || responseStatus == 504) {
response = "Service is temporarily unavailable. Please try again shortly.";
}
} catch (error) {
console.error(`${connectionLog} Error getting answer from NLP:`, error);
response = "Sorry, there was an error processing your request.";
}
console.log(`${connectionLog} Answer sent back to user ${userId}: ${response}`);
console.log(`${connectionLog} Received answer from Python at: ${new Date().toISOString()}`);
// Log the interaction
const logQuery = `
INSERT INTO interaction_logs (session_id, session_title, hospital_code, query, response, app_user_id)
VALUES (?, ?, ?, ?, ?, ?)
`;
const logResult = await db.query(logQuery, [
user_session_id,
session_title || "Chat Session",
hospital_code,
query,
response,
userId
]);
const insertId = logResult.insertId;
// Retrieve the full log entry
const selectQuery = `
SELECT * FROM interaction_logs WHERE id = ?
`;
const result = await db.query(selectQuery, [insertId]);
// Get the specific user connection and check if it's valid
const userConnection = userConnections.get(userId);
if (userConnection && userConnection.readyState === WebSocket.OPEN) {
console.log(`${connectionLog} Sending answer to user ${userId} at: ${new Date().toISOString()}`);
userConnection.send(JSON.stringify({
answer: result,
type: "chat",
sessionId: userState.activeSessionId
}));
} else {
console.log(`${connectionLog} User ${userId} connection is not open or no longer valid.`);
// Clean up the invalid connection
if (userConnections.has(userId)) {
userConnections.delete(userId);
}
}
} catch (error) {
console.error(
`[Conn: ${ws.connectionId}] Error handling WebSocket message:`,
error.message,
error.stack
);
if (ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
error: "Internal server error. Please try again later.",
details: process.env.NODE_ENV === 'development' ? error.message : undefined
})
);
}
}
});
ws.on("close", function () {
console.log(`Connection closed: ${ws.connectionId}`);
if (ws.userId) {
cleanupConnection(ws.userId);
}
});
ws.on("error", function (error) {
console.error(`WebSocket error on connection ${ws.connectionId}:`, error);
if (ws.userId) {
cleanupConnection(ws.userId);
}
});
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, closing WebSocket server...');
wss.close(() => {
console.log('WebSocket server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT received, closing WebSocket server...');
wss.close(() => {
console.log('WebSocket server closed');
process.exit(0);
});
});
module.exports = wss;

View File

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

View File

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

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

@ -0,0 +1,10 @@
/**
* Wraps an async function to handle errors consistently
* @param {Function} fn - The async function to wrap
* @returns {Function} Express middleware function
*/
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;

Some files were not shown because too many files have changed in this diff Show More