user-group.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import type { IGrantedGroup, IUser } from '@growi/core';
  2. import type { DeleteResult } from 'mongodb';
  3. import mongoose, { type Model } from 'mongoose';
  4. import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
  5. import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
  6. import type {
  7. UserGroupDocument,
  8. UserGroupModel,
  9. } from '~/server/models/user-group';
  10. import UserGroup from '~/server/models/user-group';
  11. import {
  12. excludeTestIdsFromTargetIds,
  13. includesObjectIds,
  14. } from '~/server/util/compare-objectId';
  15. import loggerFactory from '~/utils/logger';
  16. import type Crowi from '../crowi';
  17. import type {
  18. UserGroupRelationDocument,
  19. UserGroupRelationModel,
  20. } from '../models/user-group-relation';
  21. import UserGroupRelation from '../models/user-group-relation';
  22. const logger = loggerFactory('growi:service:UserGroupService');
  23. export interface IUserGroupService {
  24. init(): Promise<void>;
  25. updateGroup(
  26. id: ObjectIdLike,
  27. name?: string,
  28. description?: string,
  29. parentId?: ObjectIdLike | null,
  30. forceUpdateParents?: boolean,
  31. ): Promise<UserGroupDocument>;
  32. removeCompletelyByRootGroupId(
  33. deleteRootGroupId: ObjectIdLike,
  34. action: string,
  35. user: IUser,
  36. transferToUserGroup?: IGrantedGroup,
  37. ): Promise<DeleteResult>;
  38. removeUserByUsername(
  39. userGroupId: ObjectIdLike,
  40. username: string,
  41. ): Promise<{ user: IUser; deletedGroupsCount: number }>;
  42. }
  43. /**
  44. * the service class of UserGroupService
  45. */
  46. class UserGroupService implements IUserGroupService {
  47. crowi: Crowi;
  48. constructor(crowi: Crowi) {
  49. this.crowi = crowi;
  50. }
  51. async init(): Promise<void> {
  52. logger.debug('removing all invalid relations');
  53. return UserGroupRelation.removeAllInvalidRelations();
  54. }
  55. // ref: https://dev.growi.org/61b2cdabaa330ce7d8152844
  56. async updateGroup(
  57. id,
  58. name?: string,
  59. description?: string,
  60. parentId?: string | null,
  61. forceUpdateParents = false,
  62. ): Promise<UserGroupDocument> {
  63. const userGroup = await UserGroup.findById(id);
  64. if (userGroup == null) {
  65. throw new Error('The group does not exist');
  66. }
  67. // check if the new group name is available
  68. const isExist = (await UserGroup.countDocuments({ name })) > 0;
  69. if (userGroup.name !== name && isExist) {
  70. throw new Error('The group name is already taken');
  71. }
  72. if (name != null) {
  73. userGroup.name = name;
  74. }
  75. if (description != null) {
  76. userGroup.description = description;
  77. }
  78. // return when not update parent
  79. if (userGroup.parent === parentId) {
  80. return userGroup.save();
  81. }
  82. /*
  83. * Update parent
  84. */
  85. if (parentId === undefined) {
  86. // undefined will be ignored
  87. return userGroup.save();
  88. }
  89. // set parent to null and return when parentId is null
  90. if (parentId == null) {
  91. userGroup.parent = null;
  92. return userGroup.save();
  93. }
  94. const parent = await UserGroup.findById(parentId);
  95. if (parent == null) {
  96. // it should not be null
  97. throw Error('Parent group does not exist.');
  98. }
  99. /*
  100. * check if able to update parent or not
  101. */
  102. // throw if parent was in self and its descendants
  103. const descendantsWithTarget =
  104. await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
  105. if (
  106. includesObjectIds(
  107. descendantsWithTarget.map((d) => d._id),
  108. [parent._id],
  109. )
  110. ) {
  111. throw Error('It is not allowed to choose parent from descendant groups.');
  112. }
  113. // find users for comparison
  114. const [targetGroupUsers, parentGroupUsers] = await Promise.all([
  115. UserGroupRelation.findUserIdsByGroupId(userGroup._id),
  116. UserGroupRelation.findUserIdsByGroupId(parent._id),
  117. ]);
  118. const usersBelongsToTargetButNotParent = excludeTestIdsFromTargetIds(
  119. targetGroupUsers,
  120. parentGroupUsers,
  121. );
  122. // save if no users exist in both target and parent groups
  123. if (targetGroupUsers.length === 0 && parentGroupUsers.length === 0) {
  124. userGroup.parent = parent._id;
  125. return userGroup.save();
  126. }
  127. // add the target group's users to all ancestors
  128. if (forceUpdateParents) {
  129. const ancestorGroups =
  130. await UserGroup.findGroupsWithAncestorsRecursively(parent);
  131. const ancestorGroupIds = ancestorGroups.map((group) => group._id);
  132. await UserGroupRelation.createByGroupIdsAndUserIds(
  133. ancestorGroupIds,
  134. usersBelongsToTargetButNotParent,
  135. );
  136. }
  137. // throw if any of users in the target group is NOT included in the parent group
  138. else {
  139. const isUpdatable = usersBelongsToTargetButNotParent.length === 0;
  140. if (!isUpdatable) {
  141. throw Error(
  142. 'The parent group does not contain the users in this group.',
  143. );
  144. }
  145. }
  146. userGroup.parent = parent._id;
  147. return userGroup.save();
  148. }
  149. async removeCompletelyByRootGroupId(
  150. deleteRootGroupId,
  151. action: PageActionOnGroupDelete,
  152. user,
  153. transferToUserGroup?: IGrantedGroup,
  154. userGroupModel: Model<UserGroupDocument> & UserGroupModel = UserGroup,
  155. userGroupRelationModel: Model<UserGroupRelationDocument> &
  156. UserGroupRelationModel = UserGroupRelation,
  157. ): Promise<DeleteResult> {
  158. const rootGroup = await userGroupModel.findById(deleteRootGroupId);
  159. if (rootGroup == null) {
  160. throw new Error(
  161. `UserGroup data does not exist. id: ${deleteRootGroupId}`,
  162. );
  163. }
  164. const groupsToDelete =
  165. await userGroupModel.findGroupsWithDescendantsRecursively([rootGroup]);
  166. // 1. update page & remove all groups
  167. await this.crowi.pageService.handlePrivatePagesForGroupsToDelete(
  168. groupsToDelete,
  169. action,
  170. transferToUserGroup,
  171. user,
  172. );
  173. // 2. remove all groups
  174. const deletedGroups = await userGroupModel.deleteMany({
  175. _id: { $in: groupsToDelete.map((g) => g._id) },
  176. });
  177. // 3. remove all relations
  178. await userGroupRelationModel.removeAllByUserGroups(groupsToDelete);
  179. return deletedGroups;
  180. }
  181. async removeUserByUsername(
  182. userGroupId: ObjectIdLike,
  183. username: string,
  184. ): Promise<{ user: IUser; deletedGroupsCount: number }> {
  185. const User = mongoose.model<IUser, { findUserByUsername }>('User');
  186. const [userGroup, user] = await Promise.all([
  187. UserGroup.findById(userGroupId),
  188. User.findUserByUsername(username),
  189. ]);
  190. const groupsOfRelationsToDelete =
  191. userGroup != null
  192. ? await UserGroup.findGroupsWithDescendantsRecursively([userGroup])
  193. : [];
  194. const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map((g) => g._id);
  195. const deleteManyRes = await UserGroupRelation.deleteMany({
  196. relatedUser: user._id,
  197. relatedGroup: { $in: relatedGroupIdsToDelete },
  198. });
  199. return { user, deletedGroupsCount: deleteManyRes.deletedCount };
  200. }
  201. }
  202. export default UserGroupService;