Skip to content

Commit 75a1cdc

Browse files
authored
Merge pull request #1023 from hpcc-systems/yadhap/account-change-notifications
Send alerts when password or any account information is changed
2 parents 5d79b48 + 315707e commit 75a1cdc

File tree

3 files changed

+225
-28
lines changed

3 files changed

+225
-28
lines changed

Tombolo/server/controllers/authController.js

+56-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const axios = require("axios");
55
const logger = require("../config/logger");
66
const roleTypes = require("../config/roleTypes");
77
const models = require("../models");
8+
const moment = require("moment");
89
const {
910
generateAccessToken,
1011
generateRefreshToken,
@@ -20,8 +21,9 @@ const {
2021
setLastLogin,
2122
} = require("../utils/authUtil");
2223
const { blacklistToken } = require("../utils/tokenBlackListing");
24+
const sequelize = require("../models").sequelize;
2325

24-
const User = models.user;
26+
const User = models.user
2527
const UserRoles = models.UserRoles;
2628
const user_application = models.user_application;
2729
const Application = models.application;
@@ -337,13 +339,17 @@ const verifyEmail = async (req, res) => {
337339

338340
//Reset Password With Token - Self Requested
339341
const resetPasswordWithToken = async (req, res) => {
342+
const transaction = await sequelize.transaction();
340343
try {
341344
const { password, token, deviceInfo } = req.body;
342345

343346
// From AccountVerificationCodes table findUser ID by code, where code is resetToken
344-
const accountVerificationCode = await AccountVerificationCodes.findOne({
345-
where: { code: token },
346-
});
347+
const accountVerificationCode = await AccountVerificationCodes.findOne(
348+
{
349+
where: { code: token },
350+
},
351+
{ transaction }
352+
);
347353

348354
// If accountVerificationCode not found
349355
if (!accountVerificationCode) {
@@ -397,22 +403,26 @@ const resetPasswordWithToken = async (req, res) => {
397403
},
398404
{
399405
where: { id: user.id },
406+
transaction,
400407
}
401408
);
402409

403410
// Delete the account verification code
404411
await AccountVerificationCodes.destroy({
405412
where: { code: token },
413+
transaction,
406414
});
407415

408416
//delete password reset link
409417
await PasswordResetLinks.destroy({
410418
where: { id: token },
419+
transaction,
411420
});
412421

413422
//remove all sessions for user before initiating new session
414423
await RefreshTokens.destroy({
415424
where: { userId: user.id },
425+
transaction,
416426
});
417427

418428
// Create token id
@@ -428,15 +438,18 @@ const resetPasswordWithToken = async (req, res) => {
428438
const { iat, exp } = jwt.decode(refreshToken);
429439

430440
// Save refresh token in DB
431-
await RefreshTokens.create({
432-
id: tokenId,
433-
userId: user.id,
434-
token: refreshToken,
435-
deviceInfo,
436-
metaData: {},
437-
iat: new Date(iat * 1000),
438-
exp: new Date(exp * 1000),
439-
});
441+
await RefreshTokens.create(
442+
{
443+
id: tokenId,
444+
userId: user.id,
445+
token: refreshToken,
446+
deviceInfo,
447+
metaData: {},
448+
iat: new Date(iat * 1000),
449+
exp: new Date(exp * 1000),
450+
},
451+
{ transaction }
452+
);
440453

441454
await setTokenCookie(res, accessToken);
442455

@@ -453,13 +466,43 @@ const resetPasswordWithToken = async (req, res) => {
453466
// remove hash from user object
454467
delete userObj.hash;
455468

469+
// Send notification informing user that password has been reset
470+
const readable_notification = `ACC_CNG_${moment().format(
471+
"YYYYMMDD_HHmmss_SSS"
472+
)}`;
473+
474+
await NotificationQueue.create(
475+
{
476+
type: "email",
477+
templateName: "accountChange",
478+
notificationOrigin: "Password Reset",
479+
deliveryType: "immediate",
480+
metaData: {
481+
notificationId: readable_notification,
482+
recipientName: `${userObj.firstName} ${userObj.lastName}`,
483+
notificationOrigin: "Password Reset",
484+
subject: "Your password has been changed",
485+
mainRecipients: [userObj.email],
486+
notificationDescription: "Password Reset",
487+
changedInfo: ["password"],
488+
},
489+
createdBy: user.id,
490+
},
491+
{ transaction }
492+
);
493+
494+
// Commit the transaction
495+
await transaction.commit();
496+
456497
// Success response
457498
res.status(200).json({
458499
success: true,
459500
message: "Password updated successfully",
460501
data: userObj,
461502
});
462503
} catch (err) {
504+
// Rollback the transaction
505+
await transaction.rollback();
463506
logger.error(`Reset Temp Password: ${err.message}`);
464507
res
465508
.status(err.status || 500)

Tombolo/server/controllers/userController.js

+74-15
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const deleteUser = async (req, res) => {
5151

5252
// Patch user with ID - not password
5353
const updateBasicUserInfo = async (req, res) => {
54+
const t = await sequelize.transaction(); // Start a transaction
55+
5456
try {
5557
const { id } = req.params;
5658
const {
@@ -61,20 +63,24 @@ const updateBasicUserInfo = async (req, res) => {
6163
verifiedUser,
6264
} = req.body;
6365

64-
// Find existing user details
65-
const existingUser = await User.findOne({ where: { id } });
66+
// Find existing user details within the transaction
67+
const existingUser = await User.findOne({ where: { id }, transaction: t });
6668

6769
// If user not found
6870
if (!existingUser) {
6971
throw { status: 404, message: "User not found" };
7072
}
7173

74+
const changedInfo = [];
75+
7276
if (firstName) {
7377
existingUser.firstName = firstName;
78+
changedInfo.push("firstName");
7479
}
7580

7681
if (lastName) {
7782
existingUser.lastName = lastName;
83+
changedInfo.push("lastName");
7884
}
7985

8086
if (registrationMethod) {
@@ -85,7 +91,7 @@ const updateBasicUserInfo = async (req, res) => {
8591
existingUser.registrationStatus = registrationStatus;
8692
}
8793

88-
if (verifiedUser !== undefined || verifiedUser !== null) {
94+
if (verifiedUser !== undefined && verifiedUser !== null) {
8995
existingUser.verifiedUser = verifiedUser;
9096
}
9197

@@ -94,15 +100,44 @@ const updateBasicUserInfo = async (req, res) => {
94100
throw { status: 400, message: "No update payload provided" };
95101
}
96102

97-
// Save user with updated details
98-
const updatedUser = await existingUser.save();
103+
// Save user with updated details within the transaction
104+
const updatedUser = await existingUser.save({ transaction: t });
105+
106+
// Queue notification within the same transaction
107+
const readable_notification = `ACC_CNG_${moment().format(
108+
"YYYYMMDD_HHmmss_SSS"
109+
)}`;
110+
await NotificationQueue.create(
111+
{
112+
type: "email",
113+
templateName: "accountChange",
114+
notificationOrigin: "User Management",
115+
deliveryType: "immediate",
116+
metaData: {
117+
notificationId: readable_notification,
118+
recipientName: `${updatedUser.firstName} ${updatedUser.lastName}`,
119+
notificationOrigin: "User Management",
120+
subject: "Account Change",
121+
mainRecipients: [updatedUser.email],
122+
notificationDescription: "Account Change",
123+
changedInfo,
124+
},
125+
createdBy: req.user.id,
126+
},
127+
{ transaction: t }
128+
);
129+
130+
// Commit the transaction
131+
await t.commit();
99132

100133
res.status(200).json({
101134
success: true,
102135
message: "User updated successfully",
103136
data: updatedUser,
104137
});
105138
} catch (err) {
139+
// Rollback transaction if anything goes wrong
140+
await t.rollback();
106141
logger.error(`Update user: ${err.message}`);
107142
res
108143
.status(err.status || 500)
@@ -159,14 +194,15 @@ const getAllUsers = async (req, res) => {
159194

160195
//Update password - Ensure current password provided is correct
161196
const changePassword = async (req, res) => {
197+
const t = await sequelize.transaction(); // Start transaction
198+
162199
try {
163200
const { id } = req.params;
164201
const { currentPassword, newPassword } = req.body;
165202

166203
// Find existing user details
167-
const existingUser = await User.findOne({ where: { id } });
204+
const existingUser = await User.findOne({ where: { id }, transaction: t });
168205

169-
// If user not found
170206
if (!existingUser) {
171207
throw { status: 404, message: "User not found" };
172208
}
@@ -176,7 +212,7 @@ const changePassword = async (req, res) => {
176212
throw { status: 400, message: "Current password is incorrect" };
177213
}
178214

179-
//check for password security violations
215+
// Check for password security violations
180216
const errors = checkPasswordSecurityViolations({
181217
password: newPassword,
182218
user: existingUser,
@@ -190,31 +226,54 @@ const changePassword = async (req, res) => {
190226
const salt = bcrypt.genSaltSync(10);
191227
existingUser.hash = bcrypt.hashSync(newPassword, salt);
192228

193-
//set password expiry
229+
// Set password expiry and previous passwords
194230
setPasswordExpiry(existingUser);
195-
196-
//set previous passwords
197231
setPreviousPasswords(existingUser);
198232

199233
// Save user with updated details
200-
const updatedUser = await User.update(
234+
await User.update(
201235
{
202236
hash: existingUser.hash,
203237
metaData: existingUser.metaData,
204238
passwordExpiresAt: existingUser.passwordExpiresAt,
205239
forcePasswordReset: existingUser.forcePasswordReset,
206240
},
241+
{ where: { id }, transaction: t }
242+
);
243+
244+
// Queue notification
245+
const readable_notification = `ACC_CNG_${moment().format(
246+
"YYYYMMDD_HHmmss_SSS"
247+
)}`;
248+
await NotificationQueue.create(
207249
{
208-
where: { id },
209-
}
250+
type: "email",
251+
templateName: "accountChange",
252+
notificationOrigin: "User Management",
253+
deliveryType: "immediate",
254+
metaData: {
255+
notificationId: readable_notification,
256+
recipientName: `${existingUser.firstName} ${existingUser.lastName}`,
257+
notificationOrigin: "User Management",
258+
subject: "Account Change",
259+
mainRecipients: [existingUser.email],
260+
notificationDescription: "Account Change",
261+
changedInfo: ["password"],
262+
},
263+
createdBy: req.user.id,
264+
},
265+
{ transaction: t }
210266
);
211267

268+
// Commit transaction
269+
await t.commit();
270+
212271
res.status(200).json({
213272
success: true,
214273
message: "Password updated successfully",
215-
data: updatedUser,
216274
});
217275
} catch (err) {
276+
await t.rollback(); // Rollback on failure
218277
logger.error(`Change password: ${err.message}`);
219278
res
220279
.status(err.status || 500)

0 commit comments

Comments
 (0)