mirror of
https://github.com/strapi/strapi.git
synced 2025-12-24 21:54:24 +00:00
add batch delete for users
Signed-off-by: Pierre Noël <petersg83@gmail.com>
This commit is contained in:
parent
615dffda6f
commit
c73aadbed5
@ -214,7 +214,17 @@
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/users/:id",
|
||||
"handler": "user.delete",
|
||||
"handler": "user.deleteOne",
|
||||
"config": {
|
||||
"policies": [
|
||||
["admin::hasPermissions", ["admin::users.delete"]]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/users/batch-delete",
|
||||
"handler": "user.deleteMany",
|
||||
"config": {
|
||||
"policies": [
|
||||
["admin::hasPermissions", ["admin::users.delete"]]
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const { validateUserCreationInput, validateUserUpdateInput } = require('../validation/user');
|
||||
const {
|
||||
validateUserCreationInput,
|
||||
validateUserUpdateInput,
|
||||
validateUsersDeleteInput,
|
||||
} = require('../validation/user');
|
||||
|
||||
module.exports = {
|
||||
async create(ctx) {
|
||||
@ -90,7 +94,7 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
|
||||
async delete(ctx) {
|
||||
async deleteOne(ctx) {
|
||||
const { id } = ctx.params;
|
||||
|
||||
const deletedUser = await strapi.admin.services.user.deleteById(id);
|
||||
@ -103,4 +107,24 @@ module.exports = {
|
||||
data: strapi.admin.services.user.sanitizeUser(deletedUser),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete several users
|
||||
* @param {KoaContext} ctx - koa context
|
||||
*/
|
||||
async deleteMany(ctx) {
|
||||
const { body } = ctx.request;
|
||||
try {
|
||||
await validateUsersDeleteInput(body);
|
||||
} catch (err) {
|
||||
return ctx.badRequest('ValidationError', err);
|
||||
}
|
||||
|
||||
const users = await strapi.admin.services.user.deleteByIds(body.ids);
|
||||
const sanitizedUsers = users.map(strapi.admin.services.user.sanitizeUser);
|
||||
|
||||
return ctx.deleted({
|
||||
data: sanitizedUsers,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -70,9 +70,9 @@ module.exports = {
|
||||
|
||||
const sanitizedRole = roles.map(strapi.admin.services.role.sanitizeRole)[0] || null;
|
||||
|
||||
ctx.body = {
|
||||
return ctx.deleted({
|
||||
data: sanitizedRole,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -90,9 +90,9 @@ module.exports = {
|
||||
const roles = await strapi.admin.services.role.deleteByIds(body.ids);
|
||||
const sanitizedRoles = roles.map(strapi.admin.services.role.sanitizeRole);
|
||||
|
||||
ctx.body = {
|
||||
return ctx.deleted({
|
||||
data: sanitizedRoles,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -203,7 +203,7 @@ describe('User', () => {
|
||||
);
|
||||
});
|
||||
test('Can delete a super admin if he/she is not the last one', async () => {
|
||||
const user = { id: 11, roles: [{ code: SUPER_ADMIN_CODE }] };
|
||||
const user = { id: 2, roles: [{ code: SUPER_ADMIN_CODE }] };
|
||||
const findOne = jest.fn(() => Promise.resolve(user));
|
||||
const getSuperAdminWithUsersCount = jest.fn(() => Promise.resolve({ id: 1, usersCount: 2 }));
|
||||
const deleteFn = jest.fn(() => user);
|
||||
@ -218,6 +218,53 @@ describe('User', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteByIds', () => {
|
||||
test('Cannot delete last super admin', async () => {
|
||||
const find = jest.fn(() =>
|
||||
Promise.resolve([
|
||||
{ id: 2, roles: [{ code: SUPER_ADMIN_CODE }] },
|
||||
{ id: 3, roles: [{ code: SUPER_ADMIN_CODE }] },
|
||||
])
|
||||
);
|
||||
const getSuperAdminWithUsersCount = jest.fn(() => Promise.resolve({ id: 1, usersCount: 2 }));
|
||||
const badRequest = jest.fn();
|
||||
global.strapi = {
|
||||
query: () => ({ find }),
|
||||
admin: { services: { role: { getSuperAdminWithUsersCount } } },
|
||||
errors: { badRequest },
|
||||
};
|
||||
|
||||
try {
|
||||
await userService.deleteByIds([2, 3]);
|
||||
} catch (e) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
expect(badRequest).toHaveBeenCalledWith(
|
||||
'ValidationError',
|
||||
'You must have at least one user with super admin role.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Can delete a super admin if he/she is not the last one', async () => {
|
||||
const users = [
|
||||
{ id: 2, roles: [{ code: SUPER_ADMIN_CODE }] },
|
||||
{ id: 3, roles: [{ code: SUPER_ADMIN_CODE }] },
|
||||
];
|
||||
const find = jest.fn(() => Promise.resolve(users));
|
||||
const getSuperAdminWithUsersCount = jest.fn(() => Promise.resolve({ id: 1, usersCount: 3 }));
|
||||
const deleteFn = jest.fn(() => users);
|
||||
global.strapi = {
|
||||
query: () => ({ find, delete: deleteFn }),
|
||||
admin: { services: { role: { getSuperAdminWithUsersCount } } },
|
||||
};
|
||||
|
||||
const res = await userService.deleteByIds([2, 3]);
|
||||
expect(deleteFn).toHaveBeenCalledWith({ id_in: [2, 3] });
|
||||
expect(res).toEqual(users);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
test('Return true if the user already exists', async () => {
|
||||
const count = jest.fn(() => Promise.resolve(1));
|
||||
|
||||
@ -170,8 +170,8 @@ const searchPage = async query => {
|
||||
return strapi.query('user', 'admin').searchPage(query);
|
||||
};
|
||||
|
||||
/** Delete users
|
||||
* @param query
|
||||
/** Delete a user
|
||||
* @param id id of the user to delete
|
||||
* @returns {Promise<user>}
|
||||
*/
|
||||
const deleteById = async id => {
|
||||
@ -194,6 +194,26 @@ const deleteById = async id => {
|
||||
return strapi.query('user', 'admin').delete({ id });
|
||||
};
|
||||
|
||||
/** Delete a user
|
||||
* @param ids ids of the users to delete
|
||||
* @returns {Promise<user>}
|
||||
*/
|
||||
const deleteByIds = async ids => {
|
||||
// Check at least one super admin remains
|
||||
const usersToDelete = await strapi.query('user', 'admin').find({ id_in: ids }, ['roles']);
|
||||
const superAdminUsers = usersToDelete.filter(hasSuperAdminRole);
|
||||
if (superAdminUsers.length > 0) {
|
||||
const superAdminRole = await strapi.admin.services.role.getSuperAdminWithUsersCount();
|
||||
if (superAdminRole.usersCount === superAdminUsers.length) {
|
||||
throw strapi.errors.badRequest(
|
||||
'ValidationError',
|
||||
'You must have at least one user with super admin role.'
|
||||
);
|
||||
}
|
||||
}
|
||||
return strapi.query('user', 'admin').delete({ id_in: ids });
|
||||
};
|
||||
|
||||
/** Count the users that don't have any associated roles
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
@ -286,6 +306,7 @@ module.exports = {
|
||||
findPage,
|
||||
searchPage,
|
||||
deleteById,
|
||||
deleteByIds,
|
||||
countUsersWithoutRole,
|
||||
assignARoleToAll,
|
||||
displayWarningIfUsersDontHaveRole,
|
||||
|
||||
@ -53,28 +53,30 @@ let rq;
|
||||
*
|
||||
* N° Description
|
||||
* -------------------------------------------
|
||||
* 1. Create a user (fail/body)
|
||||
* 2. Create a user (success)
|
||||
* 3. Update a user (success)
|
||||
* 4. Create a user with superAdmin role (success)
|
||||
* 5. Update a user (fail/body)
|
||||
* 6. Get a user (success)
|
||||
* 7. Get a list of users (success/full)
|
||||
* 8. Delete a user (success)
|
||||
* 9. Delete a user (fail/notFound)
|
||||
* 10. Deletes a super admin user (successfully)
|
||||
* 11. Deletes last super admin user (bad request)
|
||||
* 12. Update a user (fail/notFound)
|
||||
* 13. Get a user (fail/notFound)
|
||||
* 14. Get a list of users (success/empty)
|
||||
* 1. Creates a user (wrong body)
|
||||
* 2. Creates a user (successfully)
|
||||
* 3. Creates users with superAdmin role (success)
|
||||
* 4. Updates a user (wrong body)
|
||||
* 5. Updates a user (successfully)
|
||||
* 6. Finds a user (successfully)
|
||||
* 7. Finds a list of users (contains user)
|
||||
* 8. Deletes a user (successfully)
|
||||
* 9. Deletes a user (not found)
|
||||
* 10. Deletes 2 super admin users (successfully)
|
||||
* 11. Deletes a super admin user (successfully)
|
||||
* 12. Deletes last super admin user (bad request)
|
||||
* 13. Deletes last super admin user in batch (bad request)
|
||||
* 14. Updates a user (not found)
|
||||
* 15. Finds a user (not found)
|
||||
* 16. Finds a list of users (missing user)
|
||||
*/
|
||||
|
||||
describe('Admin User CRUD (e2e)', () => {
|
||||
// Local test data used across the test suite
|
||||
let testData = {
|
||||
firstSuperAdminUser: undefined,
|
||||
otherSuperAdminUsers: [],
|
||||
user: undefined,
|
||||
secondSuperAdminUser: undefined,
|
||||
role: undefined,
|
||||
superAdminRole: undefined,
|
||||
};
|
||||
@ -138,25 +140,28 @@ describe('Admin User CRUD (e2e)', () => {
|
||||
testData.user = res.body.data;
|
||||
});
|
||||
|
||||
test('3. Creates a user with superAdmin role (success)', async () => {
|
||||
const body = {
|
||||
email: 'user-tests2@strapi-e2e.com',
|
||||
firstname: 'user_tests-firstname',
|
||||
lastname: 'user_tests-lastname',
|
||||
roles: [testData.superAdminRole.id],
|
||||
test('3. Creates users with superAdmin role (success)', async () => {
|
||||
const getBody = index => {
|
||||
return {
|
||||
email: `user-tests${index}@strapi-e2e.com`,
|
||||
firstname: 'user_tests-firstname',
|
||||
lastname: 'user_tests-lastname',
|
||||
roles: [testData.superAdminRole.id],
|
||||
};
|
||||
};
|
||||
|
||||
const res = await rq({
|
||||
url: '/admin/users',
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res = await rq({
|
||||
url: '/admin/users',
|
||||
method: 'POST',
|
||||
body: getBody(i),
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
expect(res.body.data).not.toBeNull();
|
||||
expect(res.statusCode).toBe(201);
|
||||
expect(res.body.data).not.toBeNull();
|
||||
|
||||
// Using the created user as an example for the rest of the tests
|
||||
testData.secondSuperAdminUser = res.body.data;
|
||||
testData.otherSuperAdminUsers.push(res.body.data);
|
||||
}
|
||||
});
|
||||
|
||||
test('4. Updates a user (wrong body)', async () => {
|
||||
@ -268,17 +273,32 @@ describe('Admin User CRUD (e2e)', () => {
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
test('10. Deletes a super admin user (successfully)', async () => {
|
||||
test('10. Deletes 2 super admin users (successfully)', async () => {
|
||||
const users = testData.otherSuperAdminUsers.splice(0, 2);
|
||||
const res = await rq({
|
||||
url: `/admin/users/${testData.secondSuperAdminUser.id}`,
|
||||
url: `/admin/users/batch-delete`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
ids: users.map(u => u.id),
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body.data).toMatchObject(users);
|
||||
});
|
||||
|
||||
test('11. Deletes a super admin user (successfully)', async () => {
|
||||
const user = testData.otherSuperAdminUsers.pop();
|
||||
const res = await rq({
|
||||
url: `/admin/users/${user.id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body.data).toMatchObject(testData.secondSuperAdminUser);
|
||||
expect(res.body.data).toMatchObject(user);
|
||||
});
|
||||
|
||||
test('11. Deletes last super admin user (bad request)', async () => {
|
||||
test('12. Deletes last super admin user (bad request)', async () => {
|
||||
const res = await rq({
|
||||
url: `/admin/users/${testData.firstSuperAdminUser.id}`,
|
||||
method: 'DELETE',
|
||||
@ -293,7 +313,25 @@ describe('Admin User CRUD (e2e)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('12. Updates a user (not found)', async () => {
|
||||
test('13. Deletes last super admin user in batch (bad request)', async () => {
|
||||
const res = await rq({
|
||||
url: `/admin/users/batch-delete`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
ids: [testData.firstSuperAdminUser.id],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.body).toMatchObject({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'ValidationError',
|
||||
data: 'You must have at least one user with super admin role.',
|
||||
});
|
||||
});
|
||||
|
||||
test('14. Updates a user (not found)', async () => {
|
||||
const body = {
|
||||
lastname: 'doe',
|
||||
};
|
||||
@ -312,7 +350,7 @@ describe('Admin User CRUD (e2e)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('13. Finds a user (not found)', async () => {
|
||||
test('15. Finds a user (not found)', async () => {
|
||||
const res = await rq({
|
||||
url: `/admin/users/${testData.user.id}`,
|
||||
method: 'GET',
|
||||
@ -326,7 +364,7 @@ describe('Admin User CRUD (e2e)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('14. Finds a list of users (missing user)', async () => {
|
||||
test('16. Finds a list of users (missing user)', async () => {
|
||||
const res = await rq({
|
||||
url: `/admin/users?email=${testData.user.email}`,
|
||||
method: 'GET',
|
||||
|
||||
@ -53,8 +53,24 @@ const validateUserUpdateInput = data => {
|
||||
return userUpdateSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);
|
||||
};
|
||||
|
||||
const usersDeleteSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
ids: yup
|
||||
.array()
|
||||
.of(yup.strapiID())
|
||||
.min(1)
|
||||
.required(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const validateUsersDeleteInput = async data => {
|
||||
return usersDeleteSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateUserCreationInput,
|
||||
validateProfileUpdateInput,
|
||||
validateUserUpdateInput,
|
||||
validateUsersDeleteInput,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user