initial commit

This commit is contained in:
rohit 2025-07-15 19:31:25 +05:30
commit 3236b348c5
55 changed files with 1429 additions and 0 deletions

28
.example.env Normal file
View File

@ -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 <noreply@guardianai.com>"
# App
PORT=3000
CLIENT_URL=http://localhost:3000
# Other
NODE_ENV=development

47
.gitignore vendored Normal file
View File

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

33
package.json Normal file
View File

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

57
server.js Normal file
View File

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

33
src/config/config.js Normal file
View File

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

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

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

20
src/config/swagger.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

24
src/middlewares/auth.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

35
src/models/alert.js Normal file
View File

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

48
src/models/call.js Normal file
View File

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

19
src/models/callType.js Normal file
View File

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

View File

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

View File

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

24
src/models/index.js Normal file
View File

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

34
src/models/llmComment.js Normal file
View File

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

26
src/models/systemStat.js Normal file
View File

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

11
src/routes/alerts.js Normal file
View File

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

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

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

12
src/routes/calls.js Normal file
View File

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

17
src/routes/caregivers.js Normal file
View File

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

11
src/routes/config.js Normal file
View File

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

1
src/routes/docs.js Normal file
View File

@ -0,0 +1 @@

17
src/routes/index.js Normal file
View File

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

11
src/routes/messages.js Normal file
View File

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

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

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

11
src/routes/settings.js Normal file
View File

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

10
src/routes/stats.js Normal file
View File

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

1
src/routes/stripe.js Normal file
View File

@ -0,0 +1 @@

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

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

View File

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

View File

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

7
src/sockets/alerts.js Normal file
View File

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

1
src/sockets/index.js Normal file
View File

@ -0,0 +1 @@

21
src/utils/email.js Normal file
View File

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