Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NSA-8672] Add bulk user actions page #519

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
18 changes: 11 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/app/users/getBulkUserActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const getBulkUserActions = (req, res) => {
const model = {
csrfToken: req.csrfToken(),
emails: '',
validationMessages: {},
};

res.render('users/views/bulkUserActions', model);
};

module.exports = getBulkUserActions;
29 changes: 29 additions & 0 deletions src/app/users/getBulkUserActionsEmails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
const { searchForBulkUsersPage } = require('./utils');

const getBulkUserActionsEmails = async (req, res) => {
const emails = req.session.emails;
if (!emails) {
return res.redirect('/users');
}
const users = [];

const emailsArray = emails.split(',');
for (const email of emailsArray) {
const result = await searchForBulkUsersPage(email.trim());
for (const user of result.users) {
users.push(user);
}
}

const model = {
csrfToken: req.csrfToken(),
users,
validationMessages: {},
};

res.render('users/views/bulkUserActionsEmails', model);
};

module.exports = getBulkUserActionsEmails;
12 changes: 10 additions & 2 deletions src/app/users/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const getOrganisationPermissions = require('./getOrganisationPermissions');
const postOrganisationPermissions = require('./postOrganisationPermissions');
const getConfirmNewUser = require('./getConfirmNewUser');
const postConfirmNewUser = require('./postConfirmNewUser');
const getBulkUserActions = require('./getBulkUserActions');
const postBulkUserActions = require('./postBulkUserActions');
const getBulkUserActionsEmails = require('./getBulkUserActionsEmails');
const postBulkUserActionsEmails = require('./postBulkUserActionsEmails');
const postCancelChangeEmail = require('./postCancelChangeEmail');
const getConfirmAssociateOrganisation = require('./getConfirmAssociateOrganisation');
const postResendInvite = require('./postResendInvite');
Expand All @@ -39,8 +43,8 @@ const postDeleteOrganisation = require('./postDeleteOrganisation');
const getSecureAccess = require('./getSecureAccessDetails');
const postUpdateAuditLog = require('./postUpdateAuditLog');
const getManageConsoleServices = require('./getManageConsoleServices');
const postManageConsoleRoles = require('./postManageConsoleRoles')
const { getManageConsoleRoles } = require('./getManageConsoleRoles')
const postManageConsoleRoles = require('./postManageConsoleRoles');
const { getManageConsoleRoles } = require('./getManageConsoleRoles');
const { get: getAssociateServices, post: postAssociateServices } = require('./associateServices');
const { get: getSelectOrganisation, post: postSelectOrganisation } = require('./selectOrganisation');
const { get: getAssociateRoles, post: postAssociateRoles } = require('./associateRoles');
Expand All @@ -67,6 +71,10 @@ const users = (csrf) => {
router.post('/organisation-permissions', csrf, asyncWrapper(postOrganisationPermissions));
router.get('/confirm-new-user', csrf, asyncWrapper(getConfirmNewUser));
router.post('/confirm-new-user', csrf, asyncWrapper(postConfirmNewUser));
router.get('/bulk-user-actions', csrf, asyncWrapper(getBulkUserActions));
router.post('/bulk-user-actions', csrf, asyncWrapper(postBulkUserActions));
router.get('/bulk-user-actions/emails', csrf, asyncWrapper(getBulkUserActionsEmails));
router.post('/bulk-user-actions/emails', csrf, asyncWrapper(postBulkUserActionsEmails));

router.get('/:uid', asyncWrapper((req, res) => {
res.redirect(`/users/${req.params.uid}/organisations`);
Expand Down
47 changes: 47 additions & 0 deletions src/app/users/postBulkUserActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable no-restricted-syntax */
const { emailPolicy } = require('login.dfe.validation');
const { sendResult } = require('../../infrastructure/utils');

const validateInput = async (req) => {
const model = {
emails: req.body.emails || '',
validationMessages: {},
};

if (!model.emails) {
model.validationMessages.emails = 'Please enter an email address';
return model;
}

// Removes any newline characters
model.emails = model.emails.replace('
', '');
model.emails = model.emails.replace('
', '');

// Trim whitespace around each email provided and remove duplicates
const trimmedEmails = model.emails.split(',').map((email) => email.trim());
const deduplicatedEmails = [...new Set(trimmedEmails)];

for (const email of deduplicatedEmails) {
if (!emailPolicy.doesEmailMeetPolicy(email)) {
model.validationMessages.emails = `Please enter a valid email address for ${email}`;
}
}

// Reglue array together back into a comma separated string
model.emails = deduplicatedEmails.join();

return model;
};

const postBulkUserActions = async (req, res) => {
const model = await validateInput(req);
if (Object.keys(model.validationMessages).length > 0) {
model.csrfToken = req.csrfToken();
return sendResult(req, res, 'users/views/bulkUserActions', model);
}

req.session.emails = model.emails;
return res.redirect('bulk-user-actions/emails');
};

module.exports = postBulkUserActions;
123 changes: 123 additions & 0 deletions src/app/users/postBulkUserActionsEmails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
const { sendResult } = require('../../infrastructure/utils');
const { deactivate, deactivateInvite } = require('../../infrastructure/directories');
const {
getUserDetailsById,
updateUserDetails,
waitForIndexToUpdate,
rejectOpenOrganisationRequestsForUser,
rejectOpenUserServiceRequestsForUser,
removeAllServicesForUser,
removeAllServicesForInvitedUser,
searchForBulkUsersPage,
} = require('./utils');

const validateInput = async (req) => {
const model = {
users: [],
validationMessages: {},
};

const reqBody = req.body;
const res = Object.keys(reqBody).filter((v) => v.startsWith('user-'));
if (res.length === 0) {
model.validationMessages.users = 'At least 1 user needs to be ticked';
}

const isDeactivateTicked = reqBody['deactivate-users'] || false;
const isRemoveServicesAndRequestsTicked = reqBody['remove-services-and-requests'] || false;
if (!isDeactivateTicked && !isRemoveServicesAndRequestsTicked) {
model.validationMessages.actions = 'At least 1 action needs to be ticked';
}

return model;
};

const updateUserIndex = async (uid, correlationId) => {
const user = await getUserDetailsById(uid, correlationId);
user.status = {
id: 0,
description: 'Deactivated',
};

await updateUserDetails(user, correlationId);
await waitForIndexToUpdate(uid, (updated) => updated.status.id === 0);
};

const updateInvitedUserIndex = async (uid, correlationId) => {
const user = await getUserDetailsById(uid, correlationId);
user.status.id = -2;
user.status.description = 'Deactivated Invitation';

await updateUserDetails(user, correlationId);
await waitForIndexToUpdate(uid, (updated) => updated.status.id === -2);
};

const deactivateUser = async (req, id) => {
await deactivate(id, req.id);
await updateUserIndex(id, req.id);
};

const deactivateInvitedUser = async (req, userId) => {
await deactivateInvite(userId, 'Bulk user deactivation', req.id);
await updateInvitedUserIndex(userId, req.id);
};

const postBulkUserActionsEmails = async (req, res) => {
const model = await validateInput(req);
if (Object.keys(model.validationMessages).length > 0) {
model.csrfToken = req.csrfToken();

// Need to search for all the users again if there's an error. It's a little inefficient,
// but realistically this page won't be generating many errors so this should be fairly infrequent.
const emails = req.session.emails;
const emailsArray = emails.split(',');
for (const email of emailsArray) {
const result = await searchForBulkUsersPage(email);
for (const user of result.users) {
model.users.push(user);
}
}
return sendResult(req, res, 'users/views/bulkUserActionsEmails', model);
}

const reqBody = req.body;
const isDeactivateTicked = reqBody['deactivate-users'] || false;
const isRemoveServicesAndRequestsTicked = reqBody['remove-services-and-requests'] || false;

// Get all the inputs and figure out which users were ticked
const tickedUsers = Object.keys(reqBody).filter(v => v.startsWith('user-'));

// eslint-disable-next-line no-restricted-syntax
for (const tickedUser of tickedUsers) {
// TODO add logging
const userId = reqBody[tickedUser];
if (isDeactivateTicked) {
if (userId.startsWith('inv-')) {
await deactivateInvitedUser(req, userId);
} else {
await deactivateUser(req, userId);
}
}

if (isRemoveServicesAndRequestsTicked) {
if (userId.startsWith('inv-')) {
await removeAllServicesForInvitedUser(userId, req);
} else {
await rejectOpenUserServiceRequestsForUser(userId, req);
await rejectOpenOrganisationRequestsForUser(userId, req);
await removeAllServicesForUser(userId, req);
}
}
}

// Clean up session value
req.session.emails = '';
const userText = tickedUsers.length > 1 ? 'users' : 'user';
res.flash('info', `Requested actions performed successfully on ${tickedUsers.length} ${userText}`);

return res.redirect('/users');
};

module.exports = postBulkUserActionsEmails;
1 change: 0 additions & 1 deletion src/app/users/postConfirmDeactivate.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const updateUserIndex = async (uid, correlationId) => {
};

await updateUserDetails(user, correlationId);

await waitForIndexToUpdate(uid, (updated) => updated.status.id === 0);
};

Expand Down
15 changes: 3 additions & 12 deletions src/app/users/postConfirmInvitationDeactivate.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable no-restricted-syntax */
const logger = require('../../infrastructure/logger');
const {
getUserDetails, getUserDetailsById, updateUserDetails, waitForIndexToUpdate,
getUserDetails, getUserDetailsById, updateUserDetails, waitForIndexToUpdate, removeAllServicesForInvitedUser,
} = require('./utils');
const { deactivateInvite } = require('../../infrastructure/directories');
const { getServicesByInvitationId, removeServiceFromInvitation } = require('../../infrastructure/access');
const { sendResult } = require('../../infrastructure/utils');

const updateUserIndex = async (uid, correlationId) => {
Expand All @@ -19,8 +18,7 @@ const updateUserIndex = async (uid, correlationId) => {

const postConfirmDeactivate = async (req, res) => {
const user = await getUserDetails(req);
const correlationId = req.id;


if (req.body['select-reason'] && req.body['select-reason'] !== 'Select a reason' && req.body.reason.trim() === '') {
req.body.reason = req.body['select-reason'];
} else if (req.body['select-reason'] && req.body['select-reason'] !== 'Select a reason' && req.body.reason.length > 0) {
Expand All @@ -40,14 +38,7 @@ const postConfirmDeactivate = async (req, res) => {
await deactivateInvite(user.id, req.body.reason, req.id);
await updateUserIndex(user.id, req.id);
if (req.body['remove-services-from-invite']) {
logger.info(`Attemping to remove services from invite with id: ${req.params.uid}`, { correlationId });
// No need to get the invitation to double check as getUserDetails does that already, if we're here then the invite definitely exists.
// getUserDetails parrots back the 'inv-<uuid>' as its id instead of giving us the true one without the 'inv-' prefix.
const invitationServiceRecords = await getServicesByInvitationId(user.id.substr(4)) || [];
for (const serviceRecord of invitationServiceRecords) {
logger.info(`Deleting invitation service record for invitationId: ${serviceRecord.invitationId}, serviceId: ${serviceRecord.serviceId} and organisationId: ${serviceRecord.organisationIdId}`, { correlationId });
removeServiceFromInvitation(serviceRecord.invitationId, serviceRecord.serviceId, serviceRecord.organisationId, req.id);
}
await removeAllServicesForInvitedUser(user.id, req);
}

logger.audit(`${req.user.email} (id: ${req.user.sub}) deactivated user invitation ${user.email} (id: ${user.id})`, {
Expand Down
Loading
Loading