user management

This commit is contained in:
rohit 2025-07-21 13:06:00 +05:30
parent 9d44572c0c
commit 38f2f64bfa
8 changed files with 275 additions and 34 deletions

View File

@ -1,6 +1,28 @@
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
const mysql = require('mysql2/promise');
const config = require('./config').db; 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( const sequelize = new Sequelize(
config.database, config.database,
config.username, config.username,

View File

@ -2,15 +2,27 @@ const { User } = require('../models');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const { jwt: jwtConfig } = require('../config/config'); 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) => { exports.register = async (req, res, next) => {
try { try {
const { name, email, password, role } = req.body; const { firstName, lastName, email, phoneNumber, password, role } = req.body;
const existing = await User.findOne({ where: { email } }); if (!['admin', 'caregiver'].includes(role)) {
if (existing) return res.status(409).json({ error: 'Email already in use' }); 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 hash = await bcrypt.hash(password, 10);
const user = await User.create({ name, email, password: hash, role }); // Set createdBy if available (e.g., if admin is creating a caregiver)
res.status(201).json({ id: user.id, name: user.name, email: user.email, role: user.role }); 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) { } catch (err) {
next(err); next(err);
} }
@ -23,8 +35,43 @@ exports.login = async (req, res, next) => {
if (!user) return res.status(401).json({ error: 'Invalid credentials' }); if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const match = await bcrypt.compare(password, user.password); const match = await bcrypt.compare(password, user.password);
if (!match) return res.status(401).json({ error: 'Invalid credentials' }); 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 }); 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) { } catch (err) {
next(err); next(err);
} }

View File

@ -1,27 +1,74 @@
// Caregivers Controller const { User } = require('../models');
exports.list = async (req, res, next) => { const bcrypt = require('bcryptjs');
// TODO: List all caregivers
res.json([]);
};
exports.create = async (req, res, next) => { exports.list = async (req, res, next) => {
// TODO: Create a new caregiver try {
res.status(201).json({}); const caregivers = await User.findAll({ where: { role: 'caregiver' }, attributes: { exclude: ['password'] } });
res.json(caregivers);
} catch (err) {
next(err);
}
}; };
exports.get = async (req, res, next) => { exports.get = async (req, res, next) => {
// TODO: Get caregiver by ID try {
res.json({}); 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) => { exports.update = async (req, res, next) => {
// TODO: Update caregiver by ID try {
res.json({}); 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) => { exports.remove = async (req, res, next) => {
// TODO: Delete caregiver by ID 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(); res.status(204).send();
} catch (err) {
next(err);
}
}; };
exports.total = async (req, res, next) => { exports.total = async (req, res, next) => {

View File

@ -1,3 +1,5 @@
const { User } = require('../models');
// Patients Controller // Patients Controller
exports.list = async (req, res, next) => { exports.list = async (req, res, next) => {
// TODO: List all patients // TODO: List all patients
@ -5,8 +7,63 @@ exports.list = async (req, res, next) => {
}; };
exports.create = async (req, res, next) => { exports.create = async (req, res, next) => {
// TODO: Create a new patient try {
res.status(201).json({}); // 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) => { exports.get = async (req, res, next) => {

View File

@ -5,7 +5,11 @@ module.exports = (sequelize, DataTypes) => {
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
}, },
name: { firstName: {
type: DataTypes.STRING,
allowNull: false,
},
lastName: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
}, },
@ -15,23 +19,77 @@ module.exports = (sequelize, DataTypes) => {
unique: true, unique: true,
validate: { isEmail: true }, validate: { isEmail: true },
}, },
password: { phoneNumber: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, 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: { role: {
type: DataTypes.ENUM('admin', 'caregiver', 'patient'), type: DataTypes.ENUM('admin', 'caregiver', 'patient'),
allowNull: false, allowNull: false,
defaultValue: 'caregiver', defaultValue: 'caregiver',
}, },
notificationOptIn: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
tierId: { tierId: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
}, },
createdBy: {
type: DataTypes.INTEGER,
allowNull: true,
},
otp: {
type: DataTypes.STRING,
allowNull: true,
},
otpExpiresAt: {
type: DataTypes.DATE,
allowNull: true,
},
}, { }, {
tableName: 'users', tableName: 'users',
timestamps: true, timestamps: true,
@ -43,6 +101,8 @@ module.exports = (sequelize, DataTypes) => {
User.hasMany(models.Call, { foreignKey: 'caregiverId', as: 'calls' }); User.hasMany(models.Call, { foreignKey: 'caregiverId', as: 'calls' });
User.hasMany(models.Call, { foreignKey: 'patientId', as: 'patientCalls' }); User.hasMany(models.Call, { foreignKey: 'patientId', as: 'patientCalls' });
User.belongsTo(models.Tier, { foreignKey: 'tierId', as: 'tier' }); 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; return User;

View File

@ -5,10 +5,12 @@ const { body } = require('express-validator');
// Register // Register
router.post('/register', [ router.post('/register', [
body('name').notEmpty(), body('firstName').notEmpty(),
body('lastName').notEmpty(),
body('email').isEmail(), body('email').isEmail(),
body('phoneNumber').notEmpty(),
body('password').isLength({ min: 6 }), body('password').isLength({ min: 6 }),
body('role').isIn(['admin', 'caregiver', 'patient']) body('role').isIn(['admin', 'caregiver'])
], authController.register); ], authController.register);
// Login // Login
@ -17,6 +19,12 @@ router.post('/login', [
body('password').notEmpty() body('password').notEmpty()
], authController.login); ], 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) // Role management (admin only)
router.patch('/role/:userId', authController.updateRole); router.patch('/role/:userId', authController.updateRole);

View File

@ -7,11 +7,11 @@ const { authenticateJWT, authorizeRoles } = require('../middlewares/auth');
router.use(authenticateJWT, authorizeRoles('admin', 'caregiver')); router.use(authenticateJWT, authorizeRoles('admin', 'caregiver'));
router.get('/', caregiversController.list); router.get('/', caregiversController.list);
router.post('/', caregiversController.create);
router.get('/:id', caregiversController.get); router.get('/:id', caregiversController.get);
router.put('/:id', caregiversController.update); router.put('/:id', caregiversController.update);
router.delete('/:id', caregiversController.remove); 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; module.exports = router;