diff --git a/src/__tests__/permissionMutations.test.ts b/src/__tests__/permissionMutations.test.ts index 535c8c289..7f6df987d 100644 --- a/src/__tests__/permissionMutations.test.ts +++ b/src/__tests__/permissionMutations.test.ts @@ -161,6 +161,9 @@ describe('Test permissions mutations', () => { }); test('Update group', async () => { + await userFactory({ groupIds: [_group._id] }); + await userFactory({ groupIds: [_group._id] }); + const user1 = await userFactory({}); const user2 = await userFactory({}); @@ -201,6 +204,9 @@ describe('Test permissions mutations', () => { } `; + await userFactory({ groupIds: [_group._id] }); + await userFactory({ groupIds: [_group._id] }); + await graphqlRequest(mutation, 'usersGroupsRemove', { _id: _group._id }, context); expect(await UsersGroups.findOne({ _id: _group._id })).toBe(null); diff --git a/src/data/logUtils.ts b/src/data/logUtils.ts index b1616b24e..a1094c5b0 100644 --- a/src/data/logUtils.ts +++ b/src/data/logUtils.ts @@ -63,6 +63,11 @@ interface IContentTypeParams { contentTypeId: string; } +/** + * @param object - Previous state of the object + * @param newData - Requested update data + * @param updatedDocument - State after any updates to the object + */ export interface ILogDataParams { type: string; description?: string; @@ -238,7 +243,7 @@ const gatherNames = async (params: ILogParams): Promise => { for (const id of uniqueIds) { const item = await collection.findOne({ _id: id }); - let name: string = ''; + let name: string = `item with id "${id}" has been deleted`; if (item) { for (const n of nameFields) { @@ -246,9 +251,9 @@ const gatherNames = async (params: ILogParams): Promise => { name = item[n]; } } - - options.push({ [foreignKey]: id, name }); } + + options.push({ [foreignKey]: id, name }); } return options; @@ -827,6 +832,25 @@ const gatherPipelineTemplateFieldNames = async ( return options; }; +const gatherUserFieldNames = async (doc: IUserDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + // show only user group names of users for now + options = await gatherNames({ + collection: UsersGroups, + idFields: doc.groupIds || [], + foreignKey: 'groupIds', + nameFields: ['name'], + prevList: options, + }); + + return options; +}; + const gatherDescriptions = async (params: IDescriptionParams): Promise => { const { action, type, obj, updatedDocument } = params; @@ -1199,6 +1223,16 @@ const gatherDescriptions = async (params: IDescriptionParams): Promise { + const { memberIds = [], oldUsers = [], group, currentUser } = params; + + for (const oldUser of oldUsers) { + const exists = memberIds.find(id => id === oldUser._id); + + if (!exists) { + const groupIds = oldUser.groupIds ? oldUser.groupIds.filter(item => item !== group._id) : []; + // user has been removed from the group + await putUpdateLog( + { + type: MODULE_NAMES.USER, + object: { _id: oldUser._id, groupIds: oldUser.groupIds }, + newData: { groupIds }, + description: `User "${oldUser.email}" has been removed from group "${group.name}"`, + updatedDocument: { groupIds }, + }, + currentUser, + ); + } + } // end oldUser loop + + for (const memberId of memberIds) { + const exists = oldUsers.find(usr => usr._id === memberId); + + // user has been added to the group + if (!exists) { + // already updated user row + const addedUser = await Users.findOne({ _id: memberId }); + + if (addedUser) { + // previous data was like this + const groupIds = (addedUser.groupIds || []).filter(groupId => groupId !== group._id); + + await putUpdateLog( + { + type: MODULE_NAMES.USER, + object: { _id: memberId, groupIds }, + newData: { groupIds: addedUser.groupIds }, + description: `User "${addedUser.email}" has been added to group ${group.name}`, + updatedDocument: { groupIds: addedUser.groupIds }, + }, + currentUser, + ); + } + } + } // end new user loop +}; + const permissionMutations = { /** * Create new permission @@ -27,7 +85,7 @@ const permissionMutations = { }, user, ); - } // end for loop + } await resetPermissionsCache(); @@ -45,7 +103,7 @@ const permissionMutations = { for (const perm of permissions) { await putDeleteLog({ type: MODULE_NAMES.PERMISSION, object: perm }, user); - } // end for loop + } await resetPermissionsCache(); @@ -61,21 +119,39 @@ const usersGroupMutations = { * @return {Promise} newly created group object */ async usersGroupsAdd(_root, { memberIds, ...doc }: IUserGroup & { memberIds?: string[] }, { user }: IContext) { - const result = await UsersGroups.createGroup(doc, memberIds); + // users before updating + const oldUsers = await Users.find({ _id: { $in: memberIds || [] } }); + + const group = await UsersGroups.createGroup(doc, memberIds); await putCreateLog( { type: MODULE_NAMES.USER_GROUP, - object: result, + object: group, newData: doc, - description: `"${result.name}" has been created`, + description: `"${group.name}" has been created`, }, user, ); + for (const oldUser of oldUsers) { + const updatedDocument = { groupIds: [...(oldUser.groupIds || []), group._id] }; + + await putUpdateLog( + { + type: MODULE_NAMES.USER, + object: oldUser, + newData: updatedDocument, + description: `User "${oldUser.email}" has been added to group ${group.name}`, + updatedDocument, + }, + user, + ); + } + await resetPermissionsCache(); - return result; + return group; }, /** @@ -90,17 +166,28 @@ const usersGroupMutations = { { user }: IContext, ) { const group = await UsersGroups.getGroup(_id); + const oldUsers = await Users.find({ groupIds: { $in: [_id] } }); const result = await UsersGroups.updateGroup(_id, doc, memberIds); - await putUpdateLog( - { - type: MODULE_NAMES.USER_GROUP, - object: group, - newData: doc, - description: `"${group.name}" has been edited`, - }, - user, - ); + // don't write unnecessary log when nothing is changed + if (group.name !== doc.name) { + await putUpdateLog( + { + type: MODULE_NAMES.USER_GROUP, + object: group, + newData: doc, + description: `"${group.name}" has been edited`, + }, + user, + ); + } + + await writeUserLog({ + currentUser: user, + memberIds, + oldUsers, + group, + }); await resetPermissionsCache(); @@ -114,6 +201,7 @@ const usersGroupMutations = { */ async usersGroupsRemove(_root, { _id }: { _id: string }, { user }: IContext) { const group = await UsersGroups.getGroup(_id); + const members = await Users.find({ groupIds: { $in: [group._id] } }); const result = await UsersGroups.removeGroup(_id); await putDeleteLog( @@ -125,6 +213,21 @@ const usersGroupMutations = { user, ); + for (const member of members) { + const groupIds = member.groupIds ? member.groupIds.filter(id => id !== group._id) : []; + + await putUpdateLog( + { + type: MODULE_NAMES.USER, + object: { _id: member._id, groupIds: member.groupIds }, + newData: { groupIds }, + updatedDocument: { groupIds }, + description: `User ${member.email} has been removed from group ${group.name}`, + }, + user, + ); + } + await resetPermissionsCache(); return result; diff --git a/src/data/resolvers/queries/logs.ts b/src/data/resolvers/queries/logs.ts index 4bb154f7b..935aafb3f 100644 --- a/src/data/resolvers/queries/logs.ts +++ b/src/data/resolvers/queries/logs.ts @@ -37,6 +37,7 @@ import { conditionSchema, segmentSchema } from '../../../db/models/definitions/s import { tagSchema } from '../../../db/models/definitions/tags'; import { taskSchema } from '../../../db/models/definitions/tasks'; import { ticketSchema } from '../../../db/models/definitions/tickets'; +import { userSchema } from '../../../db/models/definitions/users'; import { MODULE_NAMES } from '../../constants'; import { fetchLogs, ILogQueryParams } from '../../logUtils'; import { checkPermission } from '../../permissions/wrappers'; @@ -188,6 +189,10 @@ const LOG_MAPPINGS: ISchemaMap[] = [ name: MODULE_NAMES.SCRIPT, schemas: [scriptSchema], }, + { + name: MODULE_NAMES.USER, + schemas: [userSchema], + }, ]; /**