commit 3236b348c54dbea8e5c2b87b7320803c62ea57ab Author: rohit Date: Tue Jul 15 19:31:25 2025 +0530 initial commit diff --git a/.example.env b/.example.env new file mode 100644 index 0000000..65aeee9 --- /dev/null +++ b/.example.env @@ -0,0 +1,28 @@ +# Database +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=yourpassword +DB_NAME=guardian_ai + +# JWT +JWT_SECRET=your_jwt_secret +JWT_EXPIRES_IN=1d + +# Stripe +STRIPE_SECRET_KEY=your_stripe_secret +STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret + +# Nodemailer (Email) +EMAIL_HOST=smtp.example.com +EMAIL_PORT=587 +EMAIL_USER=your@email.com +EMAIL_PASS=your_email_password +EMAIL_FROM="Guardian AI " + +# App +PORT=3000 +CLIENT_URL=http://localhost:3000 + +# Other +NODE_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd43196 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Node modules +node_modules/ + +# Environment variables +.env +.env.*.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Debug +*.swp +*.swo + +# OS & Editor settings +.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# Build output +dist/ +build/ +tmp/ +temp/ + +# Database files (if local DB is used) +*.sqlite +*.sqlite3 + +# Coverage / testing +coverage/ +.nyc_output/ +jest-* + +# Misc +*.local +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/package.json b/package.json new file mode 100644 index 0000000..7a518c1 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "guardian-ai-backend", + "version": "1.0.0", + "description": "Production-ready Node.js backend for Guardian AI (Express, MySQL, Sequelize, JWT, Stripe, Socket.IO)", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "migrate": "sequelize-cli db:migrate", + "seed": "sequelize-cli db:seed:all" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.0.1", + "helmet": "^7.0.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "mysql2": "^3.6.0", + "nodemailer": "^6.9.8", + "sequelize": "^6.37.1", + "socket.io": "^4.7.5", + "stripe": "^14.23.0" + }, + "devDependencies": { + "nodemon": "^3.0.3", + "sequelize-cli": "^6.6.1" + }, + "author": "Guardian AI Team", + "license": "MIT" +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..f727501 --- /dev/null +++ b/server.js @@ -0,0 +1,57 @@ +const express = require('express'); +const http = require('http'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); +const dotenv = require('dotenv'); +const { app: appConfig } = require('./src/config/config'); +const db = require('./src/models'); +const socketio = require('socket.io'); +const path = require('path'); + +// Load env +dotenv.config(); + +const app = express(); +const server = http.createServer(app); +const io = socketio(server, { + cors: { + origin: appConfig.clientUrl, + methods: ['GET', 'POST'], + credentials: true, + }, +}); +require('./src/sockets')(io); + +// Middleware +app.use(cors({ origin: appConfig.clientUrl, credentials: true })); +app.use(helmet()); +app.use(morgan('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +// Serve static files from public +app.use(express.static(path.join(__dirname, 'public'))); + +// Attach io to app for use in routes/controllers +app.set('io', io); + +// Health check +app.get('/api/health', (req, res) => res.json({ status: 'ok' })); + +// Mount all API routes +app.use('/api/v1', require('./src/routes')); + +// Error handler +app.use((err, req, res, next) => { + console.error(err); + res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' }); +}); + +// Start server +const PORT = appConfig.port; +db.sequelize.sync().then(() => { + server.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + }); +}); \ No newline at end of file diff --git a/src/config/config.js b/src/config/config.js new file mode 100644 index 0000000..6d5ec24 --- /dev/null +++ b/src/config/config.js @@ -0,0 +1,33 @@ +require('dotenv').config(); + +module.exports = { + app: { + port: process.env.PORT || 3000, + clientUrl: process.env.CLIENT_URL || 'http://localhost:3000', + env: process.env.NODE_ENV || 'development', + }, + db: { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + dialect: 'mysql', + logging: false, + }, + jwt: { + secret: process.env.JWT_SECRET, + expiresIn: process.env.JWT_EXPIRES_IN || '1d', + }, + stripe: { + secretKey: process.env.STRIPE_SECRET_KEY, + webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, + }, + email: { + host: process.env.EMAIL_HOST, + port: process.env.EMAIL_PORT, + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + from: process.env.EMAIL_FROM, + }, +}; \ No newline at end of file diff --git a/src/config/database.js b/src/config/database.js new file mode 100644 index 0000000..a8ff4b1 --- /dev/null +++ b/src/config/database.js @@ -0,0 +1,16 @@ +const { Sequelize } = require('sequelize'); +const config = require('./config').db; + +const sequelize = new Sequelize( + config.database, + config.username, + config.password, + { + host: config.host, + port: config.port, + dialect: config.dialect, + logging: config.logging, + } +); + +module.exports = sequelize; \ No newline at end of file diff --git a/src/config/swagger.js b/src/config/swagger.js new file mode 100644 index 0000000..fc1d16d --- /dev/null +++ b/src/config/swagger.js @@ -0,0 +1,20 @@ +const swaggerJSDoc = require('swagger-jsdoc'); + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Guardian AI API', + version: '1.0.0', + description: 'API documentation for Guardian AI backend', + }, + servers: [ + { url: 'http://localhost:3000/api' }, + ], + }, + apis: ['./routes/*.js'], +}; + +const swaggerSpec = swaggerJSDoc(options); + +module.exports = swaggerSpec; \ No newline at end of file diff --git a/src/controllers/alertsController.js b/src/controllers/alertsController.js new file mode 100644 index 0000000..5757304 --- /dev/null +++ b/src/controllers/alertsController.js @@ -0,0 +1,10 @@ +// Alerts Controller +exports.list = async (req, res, next) => { + // TODO: List all alerts for caregiver + res.json([]); +}; + +exports.markRead = async (req, res, next) => { + // TODO: Mark alert as read + res.json({}); +}; \ No newline at end of file diff --git a/src/controllers/authController.js b/src/controllers/authController.js new file mode 100644 index 0000000..c850152 --- /dev/null +++ b/src/controllers/authController.js @@ -0,0 +1,46 @@ +const { User } = require('../models'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const { jwt: jwtConfig } = require('../config/config'); + +exports.register = async (req, res, next) => { + try { + const { name, email, password, role } = req.body; + const existing = await User.findOne({ where: { email } }); + if (existing) return res.status(409).json({ error: 'Email already in use' }); + const hash = await bcrypt.hash(password, 10); + const user = await User.create({ name, email, password: hash, role }); + res.status(201).json({ id: user.id, name: user.name, email: user.email, role: user.role }); + } catch (err) { + next(err); + } +}; + +exports.login = async (req, res, next) => { + try { + const { email, password } = req.body; + const user = await User.findOne({ where: { email } }); + if (!user) return res.status(401).json({ error: 'Invalid credentials' }); + const match = await bcrypt.compare(password, user.password); + if (!match) return res.status(401).json({ error: 'Invalid credentials' }); + const token = jwt.sign({ id: user.id, role: user.role }, jwtConfig.secret, { expiresIn: jwtConfig.expiresIn }); + res.json({ token, user: { id: user.id, name: user.name, email: user.email, role: user.role } }); + } catch (err) { + next(err); + } +}; + +exports.updateRole = async (req, res, next) => { + try { + const { userId } = req.params; + const { role } = req.body; + if (!['admin', 'caregiver', 'patient'].includes(role)) return res.status(400).json({ error: 'Invalid role' }); + const user = await User.findByPk(userId); + if (!user) return res.status(404).json({ error: 'User not found' }); + user.role = role; + await user.save(); + res.json({ id: user.id, role: user.role }); + } catch (err) { + next(err); + } +}; \ No newline at end of file diff --git a/src/controllers/callsController.js b/src/controllers/callsController.js new file mode 100644 index 0000000..d6db0e6 --- /dev/null +++ b/src/controllers/callsController.js @@ -0,0 +1,15 @@ +// Calls Controller +exports.create = async (req, res, next) => { + // TODO: Create a new call record + res.status(201).json({}); +}; + +exports.report = async (req, res, next) => { + // TODO: Return call reports (per day, week, month, custom) + res.json([]); +}; + +exports.analytics = async (req, res, next) => { + // TODO: Return call success/failure analytics + res.json({ success: 0, fail: 0 }); +}; \ No newline at end of file diff --git a/src/controllers/caregiversController.js b/src/controllers/caregiversController.js new file mode 100644 index 0000000..5038a85 --- /dev/null +++ b/src/controllers/caregiversController.js @@ -0,0 +1,35 @@ +// Caregivers Controller +exports.list = async (req, res, next) => { + // TODO: List all caregivers + res.json([]); +}; + +exports.create = async (req, res, next) => { + // TODO: Create a new caregiver + res.status(201).json({}); +}; + +exports.get = async (req, res, next) => { + // TODO: Get caregiver by ID + res.json({}); +}; + +exports.update = async (req, res, next) => { + // TODO: Update caregiver by ID + res.json({}); +}; + +exports.remove = async (req, res, next) => { + // TODO: Delete caregiver by ID + res.status(204).send(); +}; + +exports.total = async (req, res, next) => { + // TODO: Return total caregivers count + res.json({ total: 0 }); +}; + +exports.associatePatient = async (req, res, next) => { + // TODO: Associate caregiver with patient + res.json({}); +}; \ No newline at end of file diff --git a/src/controllers/configController.js b/src/controllers/configController.js new file mode 100644 index 0000000..0147bb0 --- /dev/null +++ b/src/controllers/configController.js @@ -0,0 +1,10 @@ +// Config Controller +exports.listCallTypes = async (req, res, next) => { + // TODO: List all call types + res.json([]); +}; + +exports.createCallType = async (req, res, next) => { + // TODO: Create a new call type + res.status(201).json({}); +}; \ No newline at end of file diff --git a/src/controllers/messagesController.js b/src/controllers/messagesController.js new file mode 100644 index 0000000..c398471 --- /dev/null +++ b/src/controllers/messagesController.js @@ -0,0 +1,10 @@ +// Messages Controller +exports.list = async (req, res, next) => { + // TODO: List all example messages + res.json([]); +}; + +exports.create = async (req, res, next) => { + // TODO: Create a new example message + res.status(201).json({}); +}; \ No newline at end of file diff --git a/src/controllers/patientsController.js b/src/controllers/patientsController.js new file mode 100644 index 0000000..147f931 --- /dev/null +++ b/src/controllers/patientsController.js @@ -0,0 +1,30 @@ +// Patients Controller +exports.list = async (req, res, next) => { + // TODO: List all patients + res.json([]); +}; + +exports.create = async (req, res, next) => { + // TODO: Create a new patient + res.status(201).json({}); +}; + +exports.get = async (req, res, next) => { + // TODO: Get patient by ID + res.json({}); +}; + +exports.update = async (req, res, next) => { + // TODO: Update patient by ID + res.json({}); +}; + +exports.remove = async (req, res, next) => { + // TODO: Delete patient by ID + res.status(204).send(); +}; + +exports.total = async (req, res, next) => { + // TODO: Return total patients count + res.json({ total: 0 }); +}; \ No newline at end of file diff --git a/src/controllers/settingsController.js b/src/controllers/settingsController.js new file mode 100644 index 0000000..d6fa672 --- /dev/null +++ b/src/controllers/settingsController.js @@ -0,0 +1,10 @@ +// Settings Controller +exports.getNotificationSetting = async (req, res, next) => { + // TODO: Get caregiver's weekly summary notification setting + res.json({ notificationOptIn: true }); +}; + +exports.setNotificationSetting = async (req, res, next) => { + // TODO: Set caregiver's weekly summary notification setting + res.json({}); +}; \ No newline at end of file diff --git a/src/controllers/statsController.js b/src/controllers/statsController.js new file mode 100644 index 0000000..c0cee6b --- /dev/null +++ b/src/controllers/statsController.js @@ -0,0 +1,5 @@ +// Stats Controller +exports.systemStats = async (req, res, next) => { + // TODO: Return system stats (SMS, emails, calls) + res.json({ smsSent: 0, emailsSent: 0, callsMade: 0 }); +}; \ No newline at end of file diff --git a/src/controllers/stripeController.js b/src/controllers/stripeController.js new file mode 100644 index 0000000..c26a01c --- /dev/null +++ b/src/controllers/stripeController.js @@ -0,0 +1,16 @@ +const stripeService = require('../services/stripeService'); + +exports.subscribe = async (req, res, next) => { + // TODO: Create Stripe subscription for caregiver + res.status(201).json({}); +}; + +exports.invoices = async (req, res, next) => { + // TODO: List invoices for caregiver + res.json([]); +}; + +exports.webhook = (req, res) => { + // TODO: Handle Stripe webhook events + stripeService.handleWebhook(req, res); +}; \ No newline at end of file diff --git a/src/controllers/tiersController.js b/src/controllers/tiersController.js new file mode 100644 index 0000000..faf6b28 --- /dev/null +++ b/src/controllers/tiersController.js @@ -0,0 +1,20 @@ +// Tiers Controller +exports.list = async (req, res, next) => { + // TODO: List all tiers + res.json([]); +}; + +exports.create = async (req, res, next) => { + // TODO: Create a new tier + res.status(201).json({}); +}; + +exports.update = async (req, res, next) => { + // TODO: Update tier by ID + res.json({}); +}; + +exports.remove = async (req, res, next) => { + // TODO: Delete tier by ID + res.status(204).send(); +}; \ No newline at end of file diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js new file mode 100644 index 0000000..79016e9 --- /dev/null +++ b/src/middlewares/auth.js @@ -0,0 +1,24 @@ +const jwt = require('jsonwebtoken'); +const { jwt: jwtConfig } = require('../config/config'); + +exports.authenticateJWT = (req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'No token provided' }); + } + const token = authHeader.split(' ')[1]; + try { + const decoded = jwt.verify(token, jwtConfig.secret); + req.user = decoded; + next(); + } catch (err) { + return res.status(401).json({ error: 'Invalid token' }); + } +}; + +exports.authorizeRoles = (...roles) => (req, res, next) => { + if (!req.user || !roles.includes(req.user.role)) { + return res.status(403).json({ error: 'Forbidden' }); + } + next(); +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-alert.js b/src/migrations/20240601-create-alert.js new file mode 100644 index 0000000..47a5126 --- /dev/null +++ b/src/migrations/20240601-create-alert.js @@ -0,0 +1,47 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('alerts', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + caregiverId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + patientId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + message: { + type: Sequelize.STRING, + allowNull: false, + }, + isRead: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('alerts'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-call-type.js b/src/migrations/20240601-create-call-type.js new file mode 100644 index 0000000..daa9990 --- /dev/null +++ b/src/migrations/20240601-create-call-type.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('call_types', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + unique: true, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('call_types'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-call.js b/src/migrations/20240601-create-call.js new file mode 100644 index 0000000..d3190b0 --- /dev/null +++ b/src/migrations/20240601-create-call.js @@ -0,0 +1,59 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('calls', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + caregiverId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + patientId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + timestamp: { + type: Sequelize.DATE, + allowNull: false, + }, + status: { + type: Sequelize.ENUM('success', 'fail'), + allowNull: false, + }, + type: { + type: Sequelize.ENUM('voice', 'video'), + allowNull: false, + }, + notes: { + type: Sequelize.STRING, + allowNull: true, + }, + duration: { + type: Sequelize.INTEGER, + allowNull: true, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('calls'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-caregiver-patient.js b/src/migrations/20240601-create-caregiver-patient.js new file mode 100644 index 0000000..53afb28 --- /dev/null +++ b/src/migrations/20240601-create-caregiver-patient.js @@ -0,0 +1,39 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('caregiver_patients', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + caregiverId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + patientId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('caregiver_patients'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-example-message.js b/src/migrations/20240601-create-example-message.js new file mode 100644 index 0000000..9976901 --- /dev/null +++ b/src/migrations/20240601-create-example-message.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('example_messages', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + text: { + type: Sequelize.STRING, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('example_messages'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-llm-comment.js b/src/migrations/20240601-create-llm-comment.js new file mode 100644 index 0000000..a1e44d5 --- /dev/null +++ b/src/migrations/20240601-create-llm-comment.js @@ -0,0 +1,44 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('llm_comments', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + patientId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: 'users', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + transcript: { + type: Sequelize.TEXT, + allowNull: false, + }, + flagged: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, + flaggedReason: { + type: Sequelize.STRING, + allowNull: true, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('llm_comments'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-system-stat.js b/src/migrations/20240601-create-system-stat.js new file mode 100644 index 0000000..7768cda --- /dev/null +++ b/src/migrations/20240601-create-system-stat.js @@ -0,0 +1,37 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('system_stats', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + smsSent: { + type: Sequelize.INTEGER, + defaultValue: 0, + }, + emailsSent: { + type: Sequelize.INTEGER, + defaultValue: 0, + }, + callsMade: { + type: Sequelize.INTEGER, + defaultValue: 0, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('system_stats'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-tier.js b/src/migrations/20240601-create-tier.js new file mode 100644 index 0000000..50651d8 --- /dev/null +++ b/src/migrations/20240601-create-tier.js @@ -0,0 +1,41 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('tiers', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + contactsLimit: { + type: Sequelize.INTEGER, + allowNull: false, + }, + price: { + type: Sequelize.DECIMAL(10,2), + allowNull: false, + }, + description: { + type: Sequelize.STRING, + allowNull: true, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('tiers'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-create-user.js b/src/migrations/20240601-create-user.js new file mode 100644 index 0000000..c3d4292 --- /dev/null +++ b/src/migrations/20240601-create-user.js @@ -0,0 +1,54 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('users', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + }, + email: { + type: Sequelize.STRING, + allowNull: false, + unique: true, + }, + password: { + type: Sequelize.STRING, + allowNull: false, + }, + role: { + type: Sequelize.ENUM('admin', 'caregiver', 'patient'), + allowNull: false, + defaultValue: 'caregiver', + }, + notificationOptIn: { + type: Sequelize.BOOLEAN, + defaultValue: true, + }, + tierId: { + type: Sequelize.INTEGER, + allowNull: true, + references: { model: 'tiers', key: 'id' }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('users'); + }, +}; \ No newline at end of file diff --git a/src/migrations/20240601-seed-initial-data.js b/src/migrations/20240601-seed-initial-data.js new file mode 100644 index 0000000..962d27a --- /dev/null +++ b/src/migrations/20240601-seed-initial-data.js @@ -0,0 +1,131 @@ +'use strict'; +const bcrypt = require('bcryptjs'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + // Insert tiers + await queryInterface.bulkInsert('tiers', [ + { + name: 'Basic', + contactsLimit: 3, + price: 9.99, + description: 'Basic plan', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: 'Premium', + contactsLimit: 10, + price: 29.99, + description: 'Premium plan', + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + + // Get tier IDs + const tiers = await queryInterface.sequelize.query('SELECT id FROM tiers;'); + const tierRows = tiers[0]; + + // Insert caregivers + const password = await bcrypt.hash('password123', 10); + await queryInterface.bulkInsert('users', [ + { + name: 'Caregiver One', + email: 'caregiver1@example.com', + password, + role: 'caregiver', + notificationOptIn: true, + tierId: tierRows[0].id, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: 'Caregiver Two', + email: 'caregiver2@example.com', + password, + role: 'caregiver', + notificationOptIn: true, + tierId: tierRows[1].id, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + + // Insert patients + await queryInterface.bulkInsert('users', [ + { + name: 'Patient One', + email: 'patient1@example.com', + password, + role: 'patient', + notificationOptIn: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + name: 'Patient Two', + email: 'patient2@example.com', + password, + role: 'patient', + notificationOptIn: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + + // Get user IDs + const users = await queryInterface.sequelize.query('SELECT id, role FROM users;'); + const caregivers = users[0].filter(u => u.role === 'caregiver'); + const patients = users[0].filter(u => u.role === 'patient'); + + // Associate caregivers and patients + await queryInterface.bulkInsert('caregiver_patients', [ + { + caregiverId: caregivers[0].id, + patientId: patients[0].id, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + caregiverId: caregivers[1].id, + patientId: patients[1].id, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + + // Insert calls + await queryInterface.bulkInsert('calls', [ + { + caregiverId: caregivers[0].id, + patientId: patients[0].id, + timestamp: new Date(), + status: 'success', + type: 'voice', + notes: 'Routine check-in', + duration: 300, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + caregiverId: caregivers[1].id, + patientId: patients[1].id, + timestamp: new Date(), + status: 'fail', + type: 'video', + notes: 'Missed call', + duration: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('calls', null, {}); + await queryInterface.bulkDelete('caregiver_patients', null, {}); + await queryInterface.bulkDelete('users', null, {}); + await queryInterface.bulkDelete('tiers', null, {}); + }, +}; \ No newline at end of file diff --git a/src/models/alert.js b/src/models/alert.js new file mode 100644 index 0000000..43774a9 --- /dev/null +++ b/src/models/alert.js @@ -0,0 +1,35 @@ +module.exports = (sequelize, DataTypes) => { + const Alert = sequelize.define('Alert', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + caregiverId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + patientId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + message: { + type: DataTypes.STRING, + allowNull: false, + }, + isRead: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + }, { + tableName: 'alerts', + timestamps: true, + }); + + Alert.associate = (models) => { + Alert.belongsTo(models.User, { foreignKey: 'caregiverId', as: 'caregiver' }); + Alert.belongsTo(models.User, { foreignKey: 'patientId', as: 'patient' }); + }; + + return Alert; +}; \ No newline at end of file diff --git a/src/models/call.js b/src/models/call.js new file mode 100644 index 0000000..761fd21 --- /dev/null +++ b/src/models/call.js @@ -0,0 +1,48 @@ +module.exports = (sequelize, DataTypes) => { + const Call = sequelize.define('Call', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + caregiverId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + patientId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + timestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + status: { + type: DataTypes.ENUM('success', 'fail'), + allowNull: false, + }, + type: { + type: DataTypes.ENUM('voice', 'video'), + allowNull: false, + }, + notes: { + type: DataTypes.STRING, + allowNull: true, + }, + duration: { + type: DataTypes.INTEGER, + allowNull: true, + comment: 'Duration in seconds', + }, + }, { + tableName: 'calls', + timestamps: true, + }); + + Call.associate = (models) => { + Call.belongsTo(models.User, { foreignKey: 'caregiverId', as: 'caregiver' }); + Call.belongsTo(models.User, { foreignKey: 'patientId', as: 'patient' }); + }; + + return Call; +}; \ No newline at end of file diff --git a/src/models/callType.js b/src/models/callType.js new file mode 100644 index 0000000..8e6142e --- /dev/null +++ b/src/models/callType.js @@ -0,0 +1,19 @@ +module.exports = (sequelize, DataTypes) => { + const CallType = sequelize.define('CallType', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + }, { + tableName: 'call_types', + timestamps: true, + }); + + return CallType; +}; \ No newline at end of file diff --git a/src/models/caregiverPatient.js b/src/models/caregiverPatient.js new file mode 100644 index 0000000..95e287f --- /dev/null +++ b/src/models/caregiverPatient.js @@ -0,0 +1,27 @@ +module.exports = (sequelize, DataTypes) => { + const CaregiverPatient = sequelize.define('CaregiverPatient', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + caregiverId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + patientId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, { + tableName: 'caregiver_patients', + timestamps: true, + }); + + CaregiverPatient.associate = (models) => { + CaregiverPatient.belongsTo(models.User, { foreignKey: 'caregiverId', as: 'caregiver' }); + CaregiverPatient.belongsTo(models.User, { foreignKey: 'patientId', as: 'patient' }); + }; + + return CaregiverPatient; +}; \ No newline at end of file diff --git a/src/models/exampleMessage.js b/src/models/exampleMessage.js new file mode 100644 index 0000000..7b2b24e --- /dev/null +++ b/src/models/exampleMessage.js @@ -0,0 +1,18 @@ +module.exports = (sequelize, DataTypes) => { + const ExampleMessage = sequelize.define('ExampleMessage', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + text: { + type: DataTypes.STRING, + allowNull: false, + }, + }, { + tableName: 'example_messages', + timestamps: true, + }); + + return ExampleMessage; +}; \ No newline at end of file diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 0000000..652b8bf --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,24 @@ +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const sequelize = require('../config/database'); + +const db = {}; + +fs.readdirSync(__dirname) + .filter(file => file !== 'index.js' && file.endsWith('.js')) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; \ No newline at end of file diff --git a/src/models/llmComment.js b/src/models/llmComment.js new file mode 100644 index 0000000..5aae88c --- /dev/null +++ b/src/models/llmComment.js @@ -0,0 +1,34 @@ +module.exports = (sequelize, DataTypes) => { + const LLMComment = sequelize.define('LLMComment', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + patientId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + transcript: { + type: DataTypes.TEXT, + allowNull: false, + }, + flagged: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + flaggedReason: { + type: DataTypes.STRING, + allowNull: true, + }, + }, { + tableName: 'llm_comments', + timestamps: true, + }); + + LLMComment.associate = (models) => { + LLMComment.belongsTo(models.User, { foreignKey: 'patientId', as: 'patient' }); + }; + + return LLMComment; +}; \ No newline at end of file diff --git a/src/models/systemStat.js b/src/models/systemStat.js new file mode 100644 index 0000000..2c7da8e --- /dev/null +++ b/src/models/systemStat.js @@ -0,0 +1,26 @@ +module.exports = (sequelize, DataTypes) => { + const SystemStat = sequelize.define('SystemStat', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + smsSent: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + emailsSent: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + callsMade: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + }, { + tableName: 'system_stats', + timestamps: true, + }); + + return SystemStat; +}; \ No newline at end of file diff --git a/src/routes/alerts.js b/src/routes/alerts.js new file mode 100644 index 0000000..dc75123 --- /dev/null +++ b/src/routes/alerts.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const alertsController = require('../controllers/alertsController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +router.use(authenticateJWT, authorizeRoles('caregiver')); + +router.get('/', alertsController.list); +router.post('/mark-read/:id', alertsController.markRead); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..f39209f --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,23 @@ +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/authController'); +const { body } = require('express-validator'); + +// Register +router.post('/register', [ + body('name').notEmpty(), + body('email').isEmail(), + body('password').isLength({ min: 6 }), + body('role').isIn(['admin', 'caregiver', 'patient']) +], authController.register); + +// Login +router.post('/login', [ + body('email').isEmail(), + body('password').notEmpty() +], authController.login); + +// Role management (admin only) +router.patch('/role/:userId', authController.updateRole); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/calls.js b/src/routes/calls.js new file mode 100644 index 0000000..0a378d7 --- /dev/null +++ b/src/routes/calls.js @@ -0,0 +1,12 @@ +const express = require('express'); +const router = express.Router(); +const callsController = require('../controllers/callsController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +router.use(authenticateJWT, authorizeRoles('admin', 'caregiver')); + +router.post('/', callsController.create); +router.get('/report', callsController.report); +router.get('/analytics', callsController.analytics); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/caregivers.js b/src/routes/caregivers.js new file mode 100644 index 0000000..fcae812 --- /dev/null +++ b/src/routes/caregivers.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); +const caregiversController = require('../controllers/caregiversController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +// All routes require caregiver or admin +router.use(authenticateJWT, authorizeRoles('admin', 'caregiver')); + +router.get('/', caregiversController.list); +router.post('/', caregiversController.create); +router.get('/:id', caregiversController.get); +router.put('/:id', caregiversController.update); +router.delete('/:id', caregiversController.remove); +router.get('/stats/total', caregiversController.total); +router.post('/:id/associate-patient', caregiversController.associatePatient); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/config.js b/src/routes/config.js new file mode 100644 index 0000000..9555f36 --- /dev/null +++ b/src/routes/config.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const configController = require('../controllers/configController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +router.use(authenticateJWT); + +router.get('/call-types', authorizeRoles('admin', 'caregiver'), configController.listCallTypes); +router.post('/call-types', authorizeRoles('admin'), configController.createCallType); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/docs.js b/src/routes/docs.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/routes/docs.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..f69072f --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); + +router.use('/auth', require('./auth')); +router.use('/caregivers', require('./caregivers')); +router.use('/patients', require('./patients')); +router.use('/calls', require('./calls')); +router.use('/tiers', require('./tiers')); +router.use('/messages', require('./messages')); +router.use('/config', require('./config')); +router.use('/stats', require('./stats')); +router.use('/alerts', require('./alerts')); +router.use('/settings', require('./settings')); +router.use('/docs', require('./docs')); +router.use('/stripe', require('./stripe')); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/messages.js b/src/routes/messages.js new file mode 100644 index 0000000..2f03915 --- /dev/null +++ b/src/routes/messages.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const messagesController = require('../controllers/messagesController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +router.use(authenticateJWT); + +router.get('/', authorizeRoles('admin', 'caregiver'), messagesController.list); +router.post('/', authorizeRoles('admin'), messagesController.create); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/patients.js b/src/routes/patients.js new file mode 100644 index 0000000..c21e6b6 --- /dev/null +++ b/src/routes/patients.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const patientsController = require('../controllers/patientsController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +// All routes require caregiver or admin +router.use(authenticateJWT, authorizeRoles('admin', 'caregiver')); + +router.get('/', patientsController.list); +router.post('/', patientsController.create); +router.get('/:id', patientsController.get); +router.put('/:id', patientsController.update); +router.delete('/:id', patientsController.remove); +router.get('/stats/total', patientsController.total); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/settings.js b/src/routes/settings.js new file mode 100644 index 0000000..c9dfbb3 --- /dev/null +++ b/src/routes/settings.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const settingsController = require('../controllers/settingsController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +router.use(authenticateJWT, authorizeRoles('caregiver')); + +router.get('/notifications', settingsController.getNotificationSetting); +router.post('/notifications', settingsController.setNotificationSetting); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/stats.js b/src/routes/stats.js new file mode 100644 index 0000000..c776441 --- /dev/null +++ b/src/routes/stats.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const statsController = require('../controllers/statsController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +router.use(authenticateJWT, authorizeRoles('admin')); + +router.get('/system', statsController.systemStats); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/stripe.js b/src/routes/stripe.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/routes/stripe.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/routes/tiers.js b/src/routes/tiers.js new file mode 100644 index 0000000..e7dbaed --- /dev/null +++ b/src/routes/tiers.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const tiersController = require('../controllers/tiersController'); +const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); + +router.use(authenticateJWT, authorizeRoles('admin')); + +router.get('/', tiersController.list); +router.post('/', tiersController.create); +router.put('/:id', tiersController.update); +router.delete('/:id', tiersController.remove); + +module.exports = router; \ No newline at end of file diff --git a/src/services/stripeService.js b/src/services/stripeService.js new file mode 100644 index 0000000..7c1437e --- /dev/null +++ b/src/services/stripeService.js @@ -0,0 +1,26 @@ +const Stripe = require('stripe'); +const { stripe: stripeConfig } = require('../config/config'); + +const stripe = Stripe(stripeConfig.secretKey); + +exports.createCustomer = async (email, name) => { + return await stripe.customers.create({ email, name }); +}; + +exports.createSubscription = async (customerId, priceId) => { + return await stripe.subscriptions.create({ + customer: customerId, + items: [{ price: priceId }], + payment_behavior: 'default_incomplete', + expand: ['latest_invoice.payment_intent'], + }); +}; + +exports.getInvoices = async (customerId) => { + return await stripe.invoices.list({ customer: customerId }); +}; + +exports.handleWebhook = (req, res) => { + // TODO: Handle Stripe webhook events + res.status(200).send('Webhook received'); +}; \ No newline at end of file diff --git a/src/services/weeklySummaryService.js b/src/services/weeklySummaryService.js new file mode 100644 index 0000000..4046203 --- /dev/null +++ b/src/services/weeklySummaryService.js @@ -0,0 +1,13 @@ +const { sendMail } = require('../utils/email'); +// const cron = require('node-cron'); // Uncomment if scheduling + +exports.sendWeeklySummary = async (caregiver, stats) => { + const subject = 'Your Weekly Call Summary'; + const text = `Hello ${caregiver.name},\n\nHere is your weekly call summary:\nCalls made: ${stats.calls}\n`; + await sendMail(caregiver.email, subject, text); +}; + +// Placeholder for scheduling +// cron.schedule('0 8 * * 1', async () => { +// // Fetch caregivers who opted in and send summary +// }); \ No newline at end of file diff --git a/src/sockets/alerts.js b/src/sockets/alerts.js new file mode 100644 index 0000000..9454a20 --- /dev/null +++ b/src/sockets/alerts.js @@ -0,0 +1,7 @@ +// Socket.IO Alerts +const emitAlert = (io, caregiverId, alert) => { + // In production, map caregiverId to socket.id (e.g., via a map or DB) + io.to(`caregiver_${caregiverId}`).emit('alert', alert); +}; + +module.exports = { emitAlert }; \ No newline at end of file diff --git a/src/sockets/index.js b/src/sockets/index.js new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/sockets/index.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/utils/email.js b/src/utils/email.js new file mode 100644 index 0000000..f52bab8 --- /dev/null +++ b/src/utils/email.js @@ -0,0 +1,21 @@ +const nodemailer = require('nodemailer'); +const { email: emailConfig } = require('../config/config'); + +const transporter = nodemailer.createTransport({ + host: emailConfig.host, + port: emailConfig.port, + auth: { + user: emailConfig.user, + pass: emailConfig.pass, + }, +}); + +exports.sendMail = async (to, subject, text, html) => { + return transporter.sendMail({ + from: emailConfig.from, + to, + subject, + text, + html, + }); +}; \ No newline at end of file