Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/controllers/notificationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const notificationController = function () {
}
if (
requestor.requestorId !== userId &&
(requestor.role !== 'Administrator' || requestor.role !== 'Owner')
requestor.role !== 'Administrator' &&
requestor.role !== 'Owner'
) {
res.status(403).send({ error: 'Unauthorized request' });
return;
Expand Down Expand Up @@ -55,7 +56,8 @@ const notificationController = function () {
}
if (
requestor.requestorId !== userId &&
(requestor.role !== 'Administrator' || requestor.role !== 'Owner')
requestor.role !== 'Administrator' &&
requestor.role !== 'Owner'
) {
res.status(403).send({ error: 'Unauthorized request' });
return;
Expand Down
46 changes: 44 additions & 2 deletions src/controllers/notificationController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,34 @@
const response = await getUserNotifications(mockReq, mockRes);
assertResMock(400, errorMsg, response, mockRes);
});
test('Ensures getUserNotifications returns error 403 if userId does not match requestorId', async () => {
test('Ensures getUserNotifications returns error 403 if userId does not match requestorId and requestor is not Admin or Owner', async () => {
const { getUserNotifications } = makeSut();
const errorMsg = { error: 'Unauthorized request' };
mockReq.body.requestor.requestorId = 'differentUserId';
mockReq.body.requestor.role = 'Volunteer';
const response = await getUserNotifications(mockReq, mockRes);
assertResMock(403, errorMsg, response, mockRes);
});
test('Ensures getUserNotifications returns 200 if userId does not match requestorId but requestor is Administrator', async () => {
const { getUserNotifications } = makeSut();
const mockNotifications = [{ id: '123', message: 'Notification Test 1' }];
mockReq.body.requestor.requestorId = 'differentUserId';
mockReq.body.requestor.role = 'Administrator';
const mockService = jest.fn().mockResolvedValue(mockNotifications);
notificationService.getNotifications = mockService;
const response = await getUserNotifications(mockReq, mockRes);

Check failure on line 63 in src/controllers/notificationController.spec.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ1wU6ttgLjtEvC7kt-b&open=AZ1wU6ttgLjtEvC7kt-b&pullRequest=2161
assertResMock(200, mockNotifications, response, mockRes);
});
test('Ensures getUserNotifications returns 200 if userId does not match requestorId but requestor is Owner', async () => {
const { getUserNotifications } = makeSut();
const mockNotifications = [{ id: '123', message: 'Notification Test 1' }];
mockReq.body.requestor.requestorId = 'differentUserId';
mockReq.body.requestor.role = 'Owner';
const mockService = jest.fn().mockResolvedValue(mockNotifications);
notificationService.getNotifications = mockService;
const response = await getUserNotifications(mockReq, mockRes);

Check failure on line 73 in src/controllers/notificationController.spec.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ1wU6ttgLjtEvC7kt-c&open=AZ1wU6ttgLjtEvC7kt-c&pullRequest=2161
assertResMock(200, mockNotifications, response, mockRes);
});
test('Ensures getUserNotifications returns 200 and notifications data when notifications are fetched successfully', async () => {
const { getUserNotifications } = makeSut();
const mockNotifications = [
Expand Down Expand Up @@ -82,13 +103,34 @@
const response = await getUnreadUserNotifications(mockReq, mockRes);
assertResMock(400, errorMsg, response, mockRes);
});
test('Ensures getUnreadUserNotifications returns error 403 if userId does not match requestorId', async () => {
test('Ensures getUnreadUserNotifications returns error 403 if userId does not match requestorId and requestor is not Admin or Owner', async () => {
const { getUnreadUserNotifications } = makeSut();
const errorMsg = { error: 'Unauthorized request' };
mockReq.body.requestor.requestorId = 'differentUserId';
mockReq.body.requestor.role = 'Volunteer';
const response = await getUnreadUserNotifications(mockReq, mockRes);
assertResMock(403, errorMsg, response, mockRes);
});
test('Ensures getUnreadUserNotifications returns 200 if userId does not match requestorId but requestor is Administrator', async () => {
const { getUnreadUserNotifications } = makeSut();
const mockNotifications = [{ id: '123', message: 'Notification Test 1' }];
mockReq.body.requestor.requestorId = 'differentUserId';
mockReq.body.requestor.role = 'Administrator';
const mockService = jest.fn().mockResolvedValue(mockNotifications);
notificationService.getUnreadUserNotifications = mockService;
const response = await getUnreadUserNotifications(mockReq, mockRes);

Check failure on line 121 in src/controllers/notificationController.spec.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ1wU6ttgLjtEvC7kt-d&open=AZ1wU6ttgLjtEvC7kt-d&pullRequest=2161
assertResMock(200, mockNotifications, response, mockRes);
});
test('Ensures getUnreadUserNotifications returns 200 if userId does not match requestorId but requestor is Owner', async () => {
const { getUnreadUserNotifications } = makeSut();
const mockNotifications = [{ id: '123', message: 'Notification Test 1' }];
mockReq.body.requestor.requestorId = 'differentUserId';
mockReq.body.requestor.role = 'Owner';
const mockService = jest.fn().mockResolvedValue(mockNotifications);
notificationService.getUnreadUserNotifications = mockService;
const response = await getUnreadUserNotifications(mockReq, mockRes);

Check failure on line 131 in src/controllers/notificationController.spec.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected `await` of a non-Promise (non-"Thenable") value.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ1wU6ttgLjtEvC7kt-e&open=AZ1wU6ttgLjtEvC7kt-e&pullRequest=2161
assertResMock(200, mockNotifications, response, mockRes);
});
test('Ensures getUnreadUserNotifications returns 200 and notifications data when notifications are fetched successfully', async () => {
const { getUnreadUserNotifications } = makeSut();
const mockNotifications = [
Expand Down
84 changes: 81 additions & 3 deletions src/controllers/userProfileController.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,13 @@

const hasChangeStatusPermission = await hasPermission(requestor, 'changeUserStatus');
const hasFinalDayPermission = await hasPermission(requestor, 'setFinalDay');
if (!(hasChangeStatusPermission && hasFinalDayPermission && canEditProtectedAccount)) {
const hasPausePermission = await hasPermission(requestor, 'interactWithPauseUserButton');
if (
!(
((hasChangeStatusPermission && hasFinalDayPermission) || hasPausePermission) &&
canEditProtectedAccount
)
) {
if (PROTECTED_EMAIL_ACCOUNT.includes(requestor.email)) {
logger.logInfo(
`Unauthorized attempt to change protected user status. Requestor: ${requestor.requestorId} Target: ${userId}`,
Expand Down Expand Up @@ -865,7 +871,12 @@
};

const getUserProfiles = async function (req, res) {
if (!(await checkPermission(req, 'getUserProfiles'))) {
if (
!(
(await checkPermission(req, 'getUserProfiles')) ||
(await checkPermission(req, 'interactWithPauseUserButton'))
)
) {
return forbidden(res, 'You are not authorized to view all users');
}

Expand Down Expand Up @@ -1587,7 +1598,7 @@

await user.save();

console.log(`✅ Saved ${key} in DB:`, user[key]);

Check warning on line 1601 in src/controllers/userProfileController.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not log user-controlled data.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ1wW2h62e01MSbbdbzO&open=AZ1wW2h62e01MSbbdbzO&pullRequest=2161

// ================================
// CACHE INVALIDATION (MERGED)
Expand Down Expand Up @@ -1839,7 +1850,6 @@
}
return null;
};

const changeUserStatus = async function (req, res) {
const { userId } = req.params;
const { action, endDate, reactivationDate } = req.body;
Expand Down Expand Up @@ -1967,6 +1977,73 @@
}
};

const pauseResumeUser = async function (req, res) {
const { userId } = req.params;
const activationDate = req.body.reactivationDate;
const status = req.body.status === 'Active';

if (!mongoose.Types.ObjectId.isValid(userId)) {
return res.status(400).send({ error: 'Bad Request' });
}

const canEditProtectedAccount = await canRequestorUpdateUser(
req.body.requestor.requestorId,
userId,
);

if (
!(
(await hasPermission(req.body.requestor, 'interactWithPauseUserButton')) &&
canEditProtectedAccount
)
) {
if (PROTECTED_EMAIL_ACCOUNT.includes(req.body.requestor.email)) {
logger.logInfo(
`Unauthorized attempt to change protected user status. Requestor: ${req.body.requestor.requestorId} Target: ${userId}`,
);
}
return res.status(403).send('You are not authorized to change user status');
}

cache.removeCache(`user-${userId}`);

try {
const user = await UserProfile.findById(userId, 'isActive email firstName lastName');
if (!user) {
return res.status(404).send({ error: 'User not found' });
}

user.set({
isActive: status,
reactivationDate: activationDate,
});

await user.save();

const isUserInCache = cache.hasCache('allusers');
if (isUserInCache) {
const allUserData = JSON.parse(cache.getCache('allusers'));
const userIdx = allUserData.findIndex((u) => u._id === userId);
if (userIdx !== -1) {
const userData = allUserData[userIdx];
userData.isActive = user.isActive;
allUserData.splice(userIdx, 1, userData);
cache.setCache('allusers', JSON.stringify(allUserData));
}
}

auditIfProtectedAccountUpdated({
requestorId: req.body.requestor.requestorId,
updatedRecordEmail: user.email,
actionPerformed: 'UserStatusUpdate',
});

return res.status(200).send({ message: 'status updated' });
} catch (error) {
return res.status(500).send(error);
}
};

const changeUserRehireableStatus = async function (req, res) {
const { userId } = req.params;
const { isRehireable } = req.body;
Expand Down Expand Up @@ -2852,6 +2929,7 @@
getTeamMembersofUser,
getProjectMembers,
changeUserStatus,
pauseResumeUser,
resetPassword,
getUserByName,
getAllUsersWithFacebookLink,
Expand Down
5 changes: 5 additions & 0 deletions src/helpers/userHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3397,6 +3397,11 @@
console.error('[Manual Resend] Error while resending:', err);
logger.logException(err);
}
async function getEmailRecipientsForStatusChange(userId) {

Check warning on line 3400 in src/helpers/userHelper.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move async function 'getEmailRecipientsForStatusChange' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AZ1wU6xwgLjtEvC7kt-g&open=AZ1wU6xwgLjtEvC7kt-g&pullRequest=2161
// Safe default: avoid breaking pause/resume if email-recipient logic is not implemented yet.
// Return empty recipients list to skip sending.
return [];
}
};

return {
Expand Down
1 change: 1 addition & 0 deletions src/routes/userProfileRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const routes = function (userProfile, project) {
.patch(controller.changeUserStatus);

userProfileRouter.route('/userProfile/name/:name').get(controller.getUserByName);
userProfileRouter.route('/userProfile/:userId/pause').patch(controller.pauseResumeUser);
userProfileRouter
.route('/userProfile/:userId/rehireable')
.patch(controller.changeUserRehireableStatus);
Expand Down
2 changes: 2 additions & 0 deletions src/utilities/createInitialPermissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ const permissionsRoles = [
'accessHgnSkillsDashboard',
'manageFAQs',
'setFinalDay',
'interactWithPauseUserButton',
],
},
{
Expand Down Expand Up @@ -297,6 +298,7 @@ const permissionsRoles = [
'manageHGNAccessSetup',
'setFinalDay',
'resendBlueSquareAndSummaryEmails',
'interactWithPauseUserButton',
],
},
];
Expand Down
Loading