Skip to content

Commit 4ad6d9d

Browse files
authored
Merge pull request #1018 from hpcc-systems/yadhap/reset-pw-for-usr
yadhap/reset-pw-for-usr
2 parents 6b51782 + 620ad2c commit 4ad6d9d

File tree

6 files changed

+211
-13
lines changed

6 files changed

+211
-13
lines changed

Tombolo/client-reactjs/src/components/admin/userManagement/Table.jsx

+51-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react';
2-
import { Table, Tooltip, Popconfirm, message, Tag } from 'antd';
3-
import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
2+
import { Table, Tooltip, Popconfirm, message, Tag, Popover } from 'antd';
3+
import { EyeOutlined, EditOutlined, DeleteOutlined, LockOutlined, DownOutlined } from '@ant-design/icons';
44

5-
import { deleteUser } from './Utils.js';
5+
import { deleteUser, resetUserPassword } from './Utils.js';
66

77
const UserManagementTable = ({
88
users,
@@ -15,6 +15,16 @@ const UserManagementTable = ({
1515
setFilteredUsers,
1616
roles,
1717
}) => {
18+
// Rest password
19+
const handlePasswordReset = async ({ id }) => {
20+
try {
21+
await resetUserPassword({ id });
22+
message.success('Password reset successfully');
23+
} catch (err) {
24+
message.error('Failed to reset password');
25+
}
26+
};
27+
1828
// Const handle user deletion - display message and setUsers and filteredUsers
1929
const handleDeleteUser = async ({ id }) => {
2030
try {
@@ -26,6 +36,7 @@ const UserManagementTable = ({
2636
message.error('Failed to delete user');
2737
}
2838
};
39+
2940
// Columns for the table
3041
const columns = [
3142
{
@@ -94,6 +105,43 @@ const UserManagementTable = ({
94105
<DeleteOutlined style={{ color: 'var(--primary)', marginRight: 15 }} />
95106
</Tooltip>
96107
</Popconfirm>
108+
109+
<Popover
110+
placement="bottom"
111+
content={
112+
<div
113+
style={{ display: 'flex', flexDirection: 'column', color: 'var(--primary)', cursor: 'pointer' }}
114+
className="jobMonitoringTable__hidden_actions">
115+
<div style={{ color: 'var(--primary)' }}>
116+
<Popconfirm
117+
title={
118+
<>
119+
<div style={{ fontWeight: 'bold' }}>{`Reset Password`} </div>
120+
<div style={{ maxWidth: 460 }}>
121+
{`Clicking "Yes" will send a password reset link to `}
122+
<Tooltip title={`${record.firstName} ${record.lastName}`}>
123+
<span style={{ color: 'var(--primary)' }}>{record.email}</span>
124+
</Tooltip>{' '}
125+
via email. Do you want to continue?`
126+
</div>
127+
</>
128+
}
129+
onConfirm={() => handlePasswordReset({ id: record.id })}
130+
okText="Yes"
131+
okButtonProps={{ danger: true }}
132+
cancelText="No"
133+
cancelButtonProps={{ type: 'primary', ghost: true }}
134+
style={{ width: '500px !important' }}>
135+
<LockOutlined style={{ marginRight: 15 }} />
136+
Reset Password
137+
</Popconfirm>
138+
</div>
139+
</div>
140+
}>
141+
<span style={{ color: 'var(--secondary)' }}>
142+
More <DownOutlined style={{ fontSize: '10px' }} />
143+
</span>
144+
</Popover>
97145
</>
98146
),
99147
},

Tombolo/client-reactjs/src/components/admin/userManagement/Utils.js

+16
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,21 @@ const bulkDeleteUsers = async ({ ids }) => {
163163
}
164164
};
165165

166+
// Reset user password
167+
const resetUserPassword = async ({ id }) => {
168+
const payload = {
169+
method: 'POST',
170+
body: JSON.stringify({ id }),
171+
headers: authHeader(),
172+
};
173+
174+
const response = await fetch(`/api/user/reset-password-for-user`, payload);
175+
176+
if (!response.ok) {
177+
throw new Error('Failed to reset password');
178+
}
179+
};
180+
166181
export {
167182
createUser,
168183
getAllUsers,
@@ -173,4 +188,5 @@ export {
173188
updateUserRoles,
174189
updateUserApplications,
175190
bulkDeleteUsers,
191+
resetUserPassword,
176192
};

Tombolo/server/controllers/userController.js

+98-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
// Imports from node modules
22
const { v4: UUIDV4 } = require("uuid");
3+
const bcrypt = require("bcryptjs");
4+
const moment = require("moment");
5+
const { v4: uuidv4 } = require("uuid");
6+
const sequelize = require("../models").sequelize;
37

48
// Local imports
59
const logger = require("../config/logger");
@@ -18,7 +22,14 @@ const UserRoles = models.UserRoles;
1822
const user_application = models.user_application;
1923
const NotificationQueue = models.notification_queue;
2024
const AccountVerificationCodes = models.AccountVerificationCodes;
25+
const PasswordResetLinks = models.PasswordResetLinks;
2126

27+
const {
28+
checkPasswordSecurityViolations,
29+
setPasswordExpiry,
30+
setPreviousPasswords,
31+
generatePassword,
32+
} = require("../utils/authUtil");
2233

2334
// Delete user with ID
2435
const deleteUser = async (req, res) => {
@@ -407,12 +418,7 @@ const createUser = async (req, res) => {
407418
}
408419

409420
// Generate random password - 12 characters - alpha numeric
410-
const charset =
411-
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
412-
let password = "";
413-
for (let i = 0; i < 12; i++) {
414-
password += charset.charAt(Math.floor(Math.random() * charset.length));
415-
}
421+
const password = generatePassword();
416422

417423
// Hash password
418424
const salt = bcrypt.genSaltSync(10);
@@ -467,7 +473,7 @@ const createUser = async (req, res) => {
467473
});
468474

469475
// Searchable notification ID
470-
const searchableNotificationId = UUIDV4();
476+
const searchableNotificationId = `USR_REG__${moment().format('YYYYMMDD_HHmmss_SSS')}`;
471477
const verificationCode = UUIDV4();
472478

473479
// Create account verification code
@@ -515,6 +521,90 @@ const createUser = async (req, res) => {
515521
}
516522
};
517523

524+
// Reset password for user
525+
const resetPasswordForUser = async (req, res) => {
526+
const transaction = await sequelize.transaction(); // Start a transaction
527+
528+
try {
529+
const { id } = req.body;
530+
531+
// Get user by ID
532+
const user = await User.findOne({ where: { id }, transaction });
533+
534+
// If user not found
535+
if (!user) {
536+
await transaction.rollback(); // Rollback if user is not found
537+
return res
538+
.status(404)
539+
.json({ success: false, message: "User not found" });
540+
}
541+
542+
// Generate a password reset token
543+
const randomId = uuidv4();
544+
const passwordRestLink = `${trimURL(process.env.WEB_URL)}/reset-password/${randomId}`;
545+
546+
// Searchable notification ID
547+
const searchableNotificationId = `USR_PWD_RST__${moment().format("YYYYMMDD_HHmmss_SSS")}`;
548+
549+
// Queue notification
550+
await NotificationQueue.create(
551+
{
552+
type: "email",
553+
templateName: "resetPasswordLink",
554+
notificationOrigin: "Reset Password",
555+
deliveryType: "immediate",
556+
createdBy: "System",
557+
updatedBy: "System",
558+
metaData: {
559+
notificationId: searchableNotificationId,
560+
recipientName: `${user.firstName}`,
561+
notificationOrigin: "Reset Password",
562+
subject: "Password Reset Link",
563+
mainRecipients: [user.email],
564+
notificationDescription: "Password Reset Link",
565+
validForHours: 24,
566+
passwordRestLink,
567+
},
568+
},
569+
{ transaction }
570+
);
571+
572+
// Save the password reset token to the user object in the database
573+
await PasswordResetLinks.create(
574+
{
575+
id: randomId,
576+
userId: user.id,
577+
resetLink: passwordRestLink,
578+
issuedAt: new Date(),
579+
expiresAt: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
580+
},
581+
{ transaction }
582+
);
583+
584+
// Create account verification code
585+
await AccountVerificationCodes.create(
586+
{
587+
code: randomId,
588+
userId: user.id,
589+
expiresAt: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
590+
},
591+
{ transaction }
592+
);
593+
594+
// Commit transaction if everything is successful
595+
await transaction.commit();
596+
597+
// Response
598+
res.status(200).json({ success: true, message: "Password reset successfully" });
599+
} catch (err) {
600+
await transaction.rollback(); // Rollback transaction in case of error
601+
logger.error(`Reset password for user: ${err.message}`);
602+
res
603+
.status(err.status || 500)
604+
.json({ success: false, message: err.message });
605+
}
606+
};
607+
518608
//Exports
519609
module.exports = {
520610
createUser,
@@ -527,4 +617,5 @@ module.exports = {
527617
bulkUpdateUsers,
528618
updateUserRoles,
529619
updateUserApplications,
620+
resetPasswordForUser,
530621
};

Tombolo/server/middlewares/userMiddleware.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,28 @@
22
const { body, param, validationResult } = require("express-validator");
33
const logger = require("../config/logger");
44

5-
// Validate user ID
5+
// Validate user ID in params
66
const validateUserId = [
77
param("id").isUUID(4).withMessage("User ID must be a valid UUID"),
88
(req, res, next) => {
99
const errors = validationResult(req);
1010
if (!errors.isEmpty()) {
11-
logger.error(`Delete user : ${errors.array()[0].msg}`);
11+
logger.error(`User Management : ${errors.array()[0].msg}`);
12+
return res
13+
.status(400)
14+
.json({ success: false, message: errors.array()[0].msg });
15+
}
16+
next();
17+
},
18+
];
19+
20+
// Validate user ID in body
21+
const validateUserIdInBody = [
22+
body("id").isUUID(4).withMessage("User ID must be a valid UUID"),
23+
(req, res, next) => {
24+
const errors = validationResult(req);
25+
if (!errors.isEmpty()) {
26+
logger.error(`User Management : ${errors.array()[0].msg}`);
1227
return res
1328
.status(400)
1429
.json({ success: false, message: errors.array()[0].msg });
@@ -198,6 +213,7 @@ const validatePatchUserRolesPayload = [
198213
module.exports = {
199214
validateManuallyCreatedUserPayload,
200215
validateUserId,
216+
validateUserIdInBody,
201217
validateUpdateUserPayload,
202218
validateChangePasswordPayload,
203219
validateBulkDeletePayload,

Tombolo/server/routes/userRoutes.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
validateBulkUpdatePayload,
1313
validatePatchUserRolesPayload,
1414
validateManuallyCreatedUserPayload,
15+
validateUserIdInBody,
1516
} = require("../middlewares/userMiddleware");
1617

1718
// Import user controller
@@ -25,7 +26,8 @@ const {
2526
bulkDeleteUsers,
2627
bulkUpdateUsers,
2728
updateUserRoles,
28-
updateUserApplications
29+
updateUserApplications,
30+
resetPasswordForUser,
2931
} = require("../controllers/userController");
3032

3133
const { validateUserRole } = require("../middlewares/rbacMiddleware");
@@ -59,5 +61,6 @@ router.patch(
5961
updateUserRoles
6062
); // Update a user by id
6163
router.patch("/applications/:id",validateUserId, updateUserApplications); // Update a user's applications
64+
router.post("/reset-password-for-user", validateUserIdInBody, resetPasswordForUser); // Reset password for user
6265
//Export
6366
module.exports = router;

Tombolo/server/utils/authUtil.js

+24
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,29 @@ const setPreviousPasswords = async (user) => {
332332
return user;
333333
};
334334

335+
// Generate a random password - 12 chars
336+
function generatePassword(length = 12) {
337+
const lowercase = "abcdefghijklmnopqrstuvwxyz";
338+
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
339+
const numbers = "0123456789";
340+
const allChars = lowercase + uppercase + numbers;
341+
342+
let password = "";
343+
344+
// Ensure at least one of each category
345+
password += lowercase.charAt(Math.floor(Math.random() * lowercase.length));
346+
password += uppercase.charAt(Math.floor(Math.random() * uppercase.length));
347+
password += numbers.charAt(Math.floor(Math.random() * numbers.length));
348+
349+
// Fill the rest randomly
350+
for (let i = 3; i < length; i++) {
351+
password += allChars.charAt(Math.floor(Math.random() * allChars.length));
352+
}
353+
354+
// Shuffle password to mix guaranteed characters
355+
return password.split("").sort(() => Math.random() - 0.5).join("");
356+
}
357+
335358
//Exports
336359
module.exports = {
337360
generateAccessToken,
@@ -348,4 +371,5 @@ module.exports = {
348371
getSupportContactEmails,
349372
getAccessRequestContactEmails,
350373
setPreviousPasswords,
374+
generatePassword,
351375
};

0 commit comments

Comments
 (0)