user management
This commit is contained in:
parent
9d44572c0c
commit
38f2f64bfa
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
res.status(204).send();
|
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) => {
|
exports.total = async (req, res, next) => {
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
Loading…
Reference in New Issue
Block a user