user management
This commit is contained in:
parent
9d44572c0c
commit
38f2f64bfa
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -3,4 +3,4 @@ module.exports = (io) => {
|
||||
// Example: socket.join(`caregiver_${userId}`) after authentication
|
||||
socket.on('disconnect', () => {});
|
||||
});
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user