diff --git a/src/config/database.js b/src/config/database.js index a8ff4b1..f274a21 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -1,6 +1,28 @@ const { Sequelize } = require('sequelize'); +const mysql = require('mysql2/promise'); const config = require('./config').db; +async function testMySQLConnection() { + try { + const connection = await mysql.createConnection({ + host: config.host, + user: config.username, + password: config.password, + port: config.port, + }); + await connection.end(); + console.log('MySQL connection successful!'); + } catch (err) { + console.error('MySQL connection failed:', err.message); + process.exit(1); + } +} + +// Immediately test connection on module load +if (require.main === module) { + testMySQLConnection(); +} + const sequelize = new Sequelize( config.database, config.username, diff --git a/src/controllers/authController.js b/src/controllers/authController.js index c850152..45a388f 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -2,15 +2,27 @@ const { User } = require('../models'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const { jwt: jwtConfig } = require('../config/config'); +const { sendMail } = require('../utils/email'); + +function generateOTP() { + return Math.floor(100000 + Math.random() * 900000).toString(); +} 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 { firstName, lastName, email, phoneNumber, password, role } = req.body; + if (!['admin', 'caregiver'].includes(role)) { + return res.status(400).json({ error: 'Invalid role' }); + } + const existingEmail = await User.findOne({ where: { email } }); + if (existingEmail) return res.status(409).json({ error: 'Email already in use' }); + const existingPhone = await User.findOne({ where: { phoneNumber } }); + if (existingPhone) return res.status(409).json({ error: 'Phone number 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 }); + // Set createdBy if available (e.g., if admin is creating a caregiver) + const createdBy = req.user && req.user.id ? req.user.id : null; + const user = await User.create({ firstName, lastName, email, phoneNumber, password: hash, role, createdBy }); + res.status(201).json({ id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, phoneNumber: user.phoneNumber, role: user.role, createdBy: user.createdBy }); } catch (err) { next(err); } @@ -23,8 +35,43 @@ exports.login = async (req, res, next) => { 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' }); + if (user.role === 'caregiver') { + // Generate OTP, save to user, send email + const otp = generateOTP(); + const otpExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 min + user.otp = otp; + user.otpExpiresAt = otpExpiresAt; + await user.save(); + await sendMail(user.email, 'Your OTP Code', `Your OTP is: ${otp}`); + return res.json({ message: 'OTP sent to your email. Please verify to complete login.' }); + } + // For admin/patient, login as usual 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 } }); + res.json({ token, user: { id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, phoneNumber: user.phoneNumber, role: user.role } }); + } catch (err) { + next(err); + } +}; + +exports.verifyOtp = async (req, res, next) => { + try { + const { email, otp } = req.body; + const user = await User.findOne({ where: { email, role: 'caregiver' } }); + if (!user || !user.otp || !user.otpExpiresAt) { + return res.status(400).json({ error: 'OTP not requested or expired' }); + } + if (user.otp !== otp) { + return res.status(401).json({ error: 'Invalid OTP' }); + } + if (user.otpExpiresAt < new Date()) { + return res.status(401).json({ error: 'OTP expired' }); + } + // Clear OTP fields + user.otp = null; + user.otpExpiresAt = null; + await user.save(); + const token = jwt.sign({ id: user.id, role: user.role }, jwtConfig.secret, { expiresIn: jwtConfig.expiresIn }); + res.json({ token, user: { id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, phoneNumber: user.phoneNumber, role: user.role } }); } catch (err) { next(err); } diff --git a/src/controllers/caregiversController.js b/src/controllers/caregiversController.js index 5038a85..db2595b 100644 --- a/src/controllers/caregiversController.js +++ b/src/controllers/caregiversController.js @@ -1,27 +1,74 @@ -// Caregivers Controller -exports.list = async (req, res, next) => { - // TODO: List all caregivers - res.json([]); -}; +const { User } = require('../models'); +const bcrypt = require('bcryptjs'); -exports.create = async (req, res, next) => { - // TODO: Create a new caregiver - res.status(201).json({}); +exports.list = async (req, res, next) => { + try { + const caregivers = await User.findAll({ where: { role: 'caregiver' }, attributes: { exclude: ['password'] } }); + res.json(caregivers); + } catch (err) { + next(err); + } }; exports.get = async (req, res, next) => { - // TODO: Get caregiver by ID - res.json({}); + try { + const caregiver = await User.findOne({ where: { id: req.params.id, role: 'caregiver' }, attributes: { exclude: ['password'] } }); + if (!caregiver) return res.status(404).json({ error: 'Caregiver not found' }); + res.json(caregiver); + } catch (err) { + next(err); + } +}; + +exports.create = async (req, res, next) => { + try { + const { firstName, lastName, email, phoneNumber, password } = req.body; + const existingEmail = await User.findOne({ where: { email } }); + if (existingEmail) return res.status(409).json({ error: 'Email already in use' }); + const existingPhone = await User.findOne({ where: { phoneNumber } }); + if (existingPhone) return res.status(409).json({ error: 'Phone number already in use' }); + const hash = await bcrypt.hash(password, 10); + const caregiver = await User.create({ firstName, lastName, email, phoneNumber, password: hash, role: 'caregiver', createdBy: req.user.id }); + res.status(201).json({ id: caregiver.id, firstName: caregiver.firstName, lastName: caregiver.lastName, email: caregiver.email, phoneNumber: caregiver.phoneNumber, role: caregiver.role, createdBy: caregiver.createdBy }); + } catch (err) { + next(err); + } }; exports.update = async (req, res, next) => { - // TODO: Update caregiver by ID - res.json({}); + try { + const caregiver = await User.findOne({ where: { id: req.params.id, role: 'caregiver' } }); + if (!caregiver) return res.status(404).json({ error: 'Caregiver not found' }); + const { firstName, lastName, email, phoneNumber, password } = req.body; + if (email && email !== caregiver.email) { + const existingEmail = await User.findOne({ where: { email } }); + if (existingEmail) return res.status(409).json({ error: 'Email already in use' }); + caregiver.email = email; + } + if (phoneNumber && phoneNumber !== caregiver.phoneNumber) { + const existingPhone = await User.findOne({ where: { phoneNumber } }); + if (existingPhone) return res.status(409).json({ error: 'Phone number already in use' }); + caregiver.phoneNumber = phoneNumber; + } + if (firstName) caregiver.firstName = firstName; + if (lastName) caregiver.lastName = lastName; + if (password) caregiver.password = await bcrypt.hash(password, 10); + await caregiver.save(); + res.json({ id: caregiver.id, firstName: caregiver.firstName, lastName: caregiver.lastName, email: caregiver.email, phoneNumber: caregiver.phoneNumber, role: caregiver.role, createdBy: caregiver.createdBy }); + } catch (err) { + next(err); + } }; exports.remove = async (req, res, next) => { - // TODO: Delete caregiver by ID - res.status(204).send(); + try { + const caregiver = await User.findOne({ where: { id: req.params.id, role: 'caregiver' } }); + if (!caregiver) return res.status(404).json({ error: 'Caregiver not found' }); + await caregiver.destroy(); + res.status(204).send(); + } catch (err) { + next(err); + } }; exports.total = async (req, res, next) => { diff --git a/src/controllers/patientsController.js b/src/controllers/patientsController.js index 147f931..8535c36 100644 --- a/src/controllers/patientsController.js +++ b/src/controllers/patientsController.js @@ -1,3 +1,5 @@ +const { User } = require('../models'); + // Patients Controller exports.list = async (req, res, next) => { // TODO: List all patients @@ -5,8 +7,63 @@ exports.list = async (req, res, next) => { }; exports.create = async (req, res, next) => { - // TODO: Create a new patient - res.status(201).json({}); + try { + // Only caregivers can add patients (enforced by route middleware) + const requiredFields = [ + 'firstName', 'lastName', 'email', 'phoneNumber', 'emergencyNumber', + 'callFrequency', 'callTime', 'retryInterval', 'maxRetry', 'contactType' + ]; + for (const field of requiredFields) { + if (!req.body[field]) { + return res.status(400).json({ error: `${field} is required` }); + } + } + // Unique email/phone check + const existingEmail = await User.findOne({ where: { email: req.body.email } }); + if (existingEmail) return res.status(409).json({ error: 'Email already in use' }); + const existingPhone = await User.findOne({ where: { phoneNumber: req.body.phoneNumber } }); + if (existingPhone) return res.status(409).json({ error: 'Phone number already in use' }); + // Create patient + const patient = await User.create({ + firstName: req.body.firstName, + lastName: req.body.lastName, + email: req.body.email, + phoneNumber: req.body.phoneNumber, + emergencyNumber: req.body.emergencyNumber, + callFrequency: req.body.callFrequency, + callTime: req.body.callTime, + retryInterval: req.body.retryInterval, + maxRetry: req.body.maxRetry, + contactType: req.body.contactType, + timeZone: req.body.timeZone, + scripts: req.body.scripts, + genderVoiceCall: req.body.genderVoiceCall, + voiceStyleCall: req.body.voiceStyleCall, + role: 'patient', + createdBy: req.user.id, + }); + res.status(201).json({ + id: patient.id, + firstName: patient.firstName, + lastName: patient.lastName, + email: patient.email, + phoneNumber: patient.phoneNumber, + emergencyNumber: patient.emergencyNumber, + callFrequency: patient.callFrequency, + callTime: patient.callTime, + retryInterval: patient.retryInterval, + maxRetry: patient.maxRetry, + contactType: patient.contactType, + timeZone: patient.timeZone, + scripts: patient.scripts, + genderVoiceCall: patient.genderVoiceCall, + voiceStyleCall: patient.voiceStyleCall, + role: patient.role, + createdBy: patient.createdBy, + }); + } catch (err) { + next(err); + } }; exports.get = async (req, res, next) => { diff --git a/src/models/user.js b/src/models/user.js index 3b20eae..7ae9b14 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -5,7 +5,11 @@ module.exports = (sequelize, DataTypes) => { autoIncrement: true, primaryKey: true, }, - name: { + firstName: { + type: DataTypes.STRING, + allowNull: false, + }, + lastName: { type: DataTypes.STRING, allowNull: false, }, @@ -15,23 +19,77 @@ module.exports = (sequelize, DataTypes) => { unique: true, validate: { isEmail: true }, }, - password: { + phoneNumber: { type: DataTypes.STRING, allowNull: false, + unique: true, + }, + emergencyNumber: { + type: DataTypes.STRING, + allowNull: true, + }, + callFrequency: { + type: DataTypes.JSON, // Array of days, e.g. ["monday", "wednesday"] + allowNull: true, + }, + callTime: { + type: DataTypes.STRING, // e.g. "14:00-15:00" + allowNull: true, + }, + retryInterval: { + type: DataTypes.ENUM('5m', '10m', '15m'), + allowNull: true, + }, + maxRetry: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { min: 1, max: 3 }, + }, + contactType: { + type: DataTypes.ENUM('senior', 'child', 'special needs'), + allowNull: true, + }, + timeZone: { + type: DataTypes.STRING, + allowNull: true, + }, + scripts: { + type: DataTypes.JSON, // { greeting, q1, q2, q3, q4, q5, closing } + allowNull: true, + }, + genderVoiceCall: { + type: DataTypes.ENUM('male', 'female'), + allowNull: true, + }, + voiceStyleCall: { + type: DataTypes.ENUM('professional', 'warm', 'casual'), + allowNull: true, + }, + password: { + type: DataTypes.STRING, + allowNull: function() { return this.role !== 'patient'; }, // password required for non-patients }, role: { type: DataTypes.ENUM('admin', 'caregiver', 'patient'), allowNull: false, defaultValue: 'caregiver', }, - notificationOptIn: { - type: DataTypes.BOOLEAN, - defaultValue: true, - }, tierId: { type: DataTypes.INTEGER, allowNull: true, }, + createdBy: { + type: DataTypes.INTEGER, + allowNull: true, + }, + otp: { + type: DataTypes.STRING, + allowNull: true, + }, + otpExpiresAt: { + type: DataTypes.DATE, + allowNull: true, + }, }, { tableName: 'users', timestamps: true, @@ -43,6 +101,8 @@ module.exports = (sequelize, DataTypes) => { User.hasMany(models.Call, { foreignKey: 'caregiverId', as: 'calls' }); User.hasMany(models.Call, { foreignKey: 'patientId', as: 'patientCalls' }); User.belongsTo(models.Tier, { foreignKey: 'tierId', as: 'tier' }); + User.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' }); + User.hasMany(models.User, { foreignKey: 'createdBy', as: 'createdUsers' }); }; return User; diff --git a/src/routes/auth.js b/src/routes/auth.js index f39209f..3b2f87a 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -5,10 +5,12 @@ const { body } = require('express-validator'); // Register router.post('/register', [ - body('name').notEmpty(), + body('firstName').notEmpty(), + body('lastName').notEmpty(), body('email').isEmail(), + body('phoneNumber').notEmpty(), body('password').isLength({ min: 6 }), - body('role').isIn(['admin', 'caregiver', 'patient']) + body('role').isIn(['admin', 'caregiver']) ], authController.register); // Login @@ -17,6 +19,12 @@ router.post('/login', [ body('password').notEmpty() ], authController.login); +// Caregiver OTP verification +router.post('/verify-otp', [ + body('email').isEmail(), + body('otp').isLength({ min: 6, max: 6 }) +], authController.verifyOtp); + // Role management (admin only) router.patch('/role/:userId', authController.updateRole); diff --git a/src/routes/caregivers.js b/src/routes/caregivers.js index fcae812..29245b3 100644 --- a/src/routes/caregivers.js +++ b/src/routes/caregivers.js @@ -7,11 +7,11 @@ const { authenticateJWT, authorizeRoles } = require('../middlewares/auth'); 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); + +// Only admin can create caregivers directly +router.post('/', authenticateJWT, authorizeRoles('admin'), caregiversController.create); module.exports = router; \ No newline at end of file diff --git a/src/sockets/index.js b/src/sockets/index.js index 1babe7a..fad686e 100644 --- a/src/sockets/index.js +++ b/src/sockets/index.js @@ -3,4 +3,4 @@ module.exports = (io) => { // Example: socket.join(`caregiver_${userId}`) after authentication socket.on('disconnect', () => {}); }); -}; \ No newline at end of file +}; \ No newline at end of file