feat: add messaging toggles, password reset SMS, and payment discounts

Allow SuperAdmin to disable SMS, email, and bot from dashboard settings on top of env flags. Add admin password reset with credentials SMS, plus payment discounts, notes, and payable amount handling.
This commit is contained in:
2026-08-16 06:58:08 +03:30
parent a6c760c3b7
commit 72be01fee3
25 changed files with 601 additions and 43 deletions
+26
View File
@@ -0,0 +1,26 @@
'use strict';
const AppError = require('../../utils/AppError');
const assertCanResetPasswordAndSms = (user) => {
if (!user) throw new AppError('USER_NOT_FOUND');
const phoneNumber = String(user.phoneNumber || user.phone || '').trim();
const username = String(user.username || '').trim();
const name = String(user.name || '').trim();
if (!phoneNumber) throw new AppError('PHONE_NUMBER_REQUIRED');
if (!username) throw new AppError('VALIDATION_FAILED', null, 'Username is missing');
return { username, phoneNumber, name };
};
const isCredentialsSmsDelivered = (sendResult) => {
if (!sendResult) return false;
return sendResult.result?.skipped !== true;
};
module.exports = {
assertCanResetPasswordAndSms,
isCredentialsSmsDelivered
};
+56
View File
@@ -0,0 +1,56 @@
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
assertCanResetPasswordAndSms,
isCredentialsSmsDelivered
} = require('./passwordReset');
describe('assertCanResetPasswordAndSms', () => {
it('throws USER_NOT_FOUND when the user is missing', () => {
assert.throws(() => assertCanResetPasswordAndSms(null), { errorCode: 'USER_NOT_FOUND' });
assert.throws(() => assertCanResetPasswordAndSms(undefined), { errorCode: 'USER_NOT_FOUND' });
});
it('throws PHONE_NUMBER_REQUIRED when no phone number is stored', () => {
assert.throws(
() => assertCanResetPasswordAndSms({ username: 'u123456' }),
{ errorCode: 'PHONE_NUMBER_REQUIRED' }
);
assert.throws(
() => assertCanResetPasswordAndSms({ username: 'u123456', phoneNumber: ' ' }),
{ errorCode: 'PHONE_NUMBER_REQUIRED' }
);
});
it('throws VALIDATION_FAILED when username is missing', () => {
assert.throws(
() => assertCanResetPasswordAndSms({ phoneNumber: '09120000000' }),
{ errorCode: 'VALIDATION_FAILED' }
);
});
it('returns the stored username and phone number', () => {
assert.deepEqual(
assertCanResetPasswordAndSms({
username: ' u482910 ',
phoneNumber: ' 09120000000 ',
name: 'علی'
}),
{ username: 'u482910', phoneNumber: '09120000000', name: 'علی' }
);
});
});
describe('isCredentialsSmsDelivered', () => {
it('is false when sending was skipped or missing', () => {
assert.equal(isCredentialsSmsDelivered(null), false);
assert.equal(isCredentialsSmsDelivered({ result: { skipped: true } }), false);
});
it('is true when the SMS sender did not skip delivery', () => {
assert.equal(isCredentialsSmsDelivered({ result: { status: 1 } }), true);
assert.equal(isCredentialsSmsDelivered({}), true);
});
});
+5
View File
@@ -56,6 +56,11 @@ exports.search = catchAsync(async (req, res, next) => {
return listResponse(res, 200, data, meta);
});
exports.resetPasswordAndSendSms = catchAsync(async (req, res, next) => {
const result = await userService.resetPasswordAndSendSms(req.params.id);
return successResponse(res, 200, 'Password reset and credentials SMS processed', result);
});
exports.enroll = catchAsync(async (req, res, next) => {
const { userId } = req.params;
const { courseId } = req.body;
+1
View File
@@ -25,6 +25,7 @@ router.get('/admin/get-all', authMiddleware, perm.requires(PERMISSIONS.USERS_REA
router.get('/admin/search', authMiddleware, perm.requires(PERMISSIONS.USERS_SEARCH), userController.search);
router.get('/admin/get-one/:id', authMiddleware, perm.requires(PERMISSIONS.USERS_READ), userController.getOne);
router.put('/admin/update/:id', authMiddleware, perm.requires(PERMISSIONS.USERS_UPDATE), validateUpdateUser, userController.update);
router.post('/admin/:id/reset-password-sms', authMiddleware, perm.requires(PERMISSIONS.USERS_UPDATE), userController.resetPasswordAndSendSms);
router.delete('/admin/delete/:id', authMiddleware, perm.requires(PERMISSIONS.USERS_DELETE), userController.delete);
router.post('/admin/:userId/enroll', authMiddleware, perm.requires(PERMISSIONS.USERS_ENROLL), validateEnroll, userController.enroll);
+37 -1
View File
@@ -8,10 +8,14 @@ const bcrypt = require('bcryptjs');
const AppError = require('../../utils/AppError');
const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination');
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
const { sendAccountCreatedSms, sendPasswordResetSms } = require('../../utils/senders/smsMessages');
const logger = require('../../utils/logger');
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
const { pickNotifyFlags } = require('../../utils/notifyFlags');
const {
assertCanResetPasswordAndSms,
isCredentialsSmsDelivered
} = require('./passwordReset');
const normalizeAdminNotes = (value) => {
if (value == null) return undefined;
@@ -271,6 +275,37 @@ const deleteUser = async (id) => {
if (!user) throw new AppError('USER_NOT_FOUND');
};
const resetPasswordAndSendSms = async (id) => {
const user = await User.findById(id);
if (!user) throw new AppError('USER_NOT_FOUND');
const { username, phoneNumber } = assertCanResetPasswordAndSms(user);
const plainPassword = generateSimplePassword();
user.passwordHash = await bcrypt.hash(plainPassword, 10);
user.refreshTokens = [];
await user.save();
let smsSent = false;
try {
const sendResult = await sendPasswordResetSms(phoneNumber, username, plainPassword, user._id);
smsSent = isCredentialsSmsDelivered(sendResult);
} catch (err) {
logger.error(`[resetPasswordAndSendSms] SMS failed for user=${user._id}: ${err.message}`);
}
logger.info(`[resetPasswordAndSendSms] Password reset for user=${user._id} smsSent=${smsSent}`);
return {
username,
phoneNumber,
smsSent,
generatedCredentials: {
username,
password: plainPassword
}
};
};
const enrollUserInCourse = async (userId, courseId) => {
const user = await User.findById(userId);
if (!user) throw new AppError('USER_NOT_FOUND');
@@ -290,5 +325,6 @@ module.exports = {
createUserAdmin,
updateUser,
deleteUser,
resetPasswordAndSendSms,
enrollUserInCourse
};