Просмотр исходного кода

Merge pull request #7766 from weseek/imprv/105325-117014-delete-completely-user-page

imprv: Recreate user homepage on new user registration and allow deletion of user homepage on user deletion
Ryoji Shimizu 2 лет назад
Родитель
Сommit
ceb23c62d8

+ 6 - 6
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -38,7 +38,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
       isShowRestrictedByGroup: false,
-      isUserPageDeletionEnabled: false,
+      isUsersHomePageDeletionEnabled: false,
       isLocalEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,
@@ -74,7 +74,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       currentPageRecursiveCompleteDeletionAuthority: generalSetting.pageRecursiveCompleteDeletionAuthority,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
-      isUserPageDeletionEnabled: generalSetting.isUserPageDeletionEnabled,
+      isUsersHomePageDeletionEnabled: generalSetting.isUsersHomePageDeletionEnabled,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
       disableLinkSharing: shareLinkSetting.disableLinkSharing,
@@ -196,10 +196,10 @@ export default class AdminGeneralSecurityContainer extends Container {
   }
 
   /**
-   * Switch isUserPageDeletionEnabled
+   * Switch isUsersHomePageDeletionEnabled
    */
-  switchisUserPageDeletionEnabled() {
-    this.setState({ isUserPageDeletionEnabled: !this.state.isUserPageDeletionEnabled });
+  switchIsUsersHomePageDeletionEnabled() {
+    this.setState({ isUsersHomePageDeletionEnabled: !this.state.isUsersHomePageDeletionEnabled });
   }
 
   /**
@@ -218,7 +218,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       pageRecursiveCompleteDeletionAuthority: this.state.currentPageRecursiveCompleteDeletionAuthority,
       hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
       hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
-      isUserPageDeletionEnabled: this.state.isUserPageDeletionEnabled,
+      isUsersHomePageDeletionEnabled: this.state.isUsersHomePageDeletionEnabled,
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);

+ 2 - 2
apps/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -461,8 +461,8 @@ class SecuritySetting extends React.Component {
                 type="checkbox"
                 className="custom-control-input"
                 id="is-user-page-deletion-enabled"
-                checked={adminGeneralSecurityContainer.state.isUserPageDeletionEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchisUserPageDeletionEnabled() }}
+                checked={adminGeneralSecurityContainer.state.isUsersHomePageDeletionEnabled}
+                onChange={() => { adminGeneralSecurityContainer.switchIsUsersHomePageDeletionEnabled() }}
               />
               <label className="custom-control-label" htmlFor="is-user-page-deletion-enabled">
                 {t('security_settings.user_home_page_deletion.enable_user_home_page_deletion')}

+ 2 - 3
apps/app/src/server/crowi/index.js

@@ -17,7 +17,7 @@ import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
-
+import UserEvent from '../events/user';
 import Activity from '../models/activity';
 import PageRedirect from '../models/page-redirect';
 import Tag from '../models/tag';
@@ -36,7 +36,6 @@ import { SlackIntegrationService } from '../service/slack-integration';
 import { UserNotificationService } from '../service/user-notification';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 
-
 const logger = loggerFactory('growi:crowi');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 const models = require('../models');
@@ -97,7 +96,7 @@ function Crowi() {
   this.port = this.env.PORT || 3000;
 
   this.events = {
-    user: new (require('../events/user'))(this),
+    user: new UserEvent(this),
     page: new (require('../events/page'))(this),
     activity: new (require('../events/activity'))(this),
     bookmark: new (require('../events/bookmark'))(this),

+ 0 - 35
apps/app/src/server/events/user.js

@@ -1,35 +0,0 @@
-const debug = require('debug')('growi:events:user');
-const util = require('util');
-const events = require('events');
-
-function UserEvent(crowi) {
-  this.crowi = crowi;
-
-  events.EventEmitter.call(this);
-}
-util.inherits(UserEvent, events.EventEmitter);
-
-UserEvent.prototype.onActivated = async function(user) {
-  const Page = this.crowi.model('Page');
-
-  const userPagePath = Page.getUserPagePath(user);
-
-  const page = await Page.findByPath(userPagePath, user);
-
-  if (page == null) {
-    const body = `# ${user.username}\nThis is ${user.username}'s page`;
-
-    // create user page
-    try {
-      await this.crowi.pageService.create(userPagePath, body, user, {});
-
-      // page created
-      debug('User page created', page);
-    }
-    catch (err) {
-      debug('Failed to create user page', err);
-    }
-  }
-};
-
-module.exports = UserEvent;

+ 50 - 0
apps/app/src/server/events/user.ts

@@ -0,0 +1,50 @@
+import EventEmitter from 'events';
+
+import type { IUserHasId } from '@growi/core';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:events:user');
+
+class UserEvent extends EventEmitter {
+
+  crowi: any;
+
+  constructor(crowi: any) {
+    super();
+    this.crowi = crowi;
+  }
+
+  async onActivated(user: IUserHasId): Promise<void> {
+    if (this.crowi.pageService === null) {
+      logger.warn('crowi pageService is null');
+      return;
+    }
+
+    const Page = this.crowi.model('Page');
+    const userHomePagePath = `/user/${user.username}`;
+    // TODO: Delete user arg.
+    // see: https://redmine.weseek.co.jp/issues/124326
+    let page = await Page.findByPath(userHomePagePath, user);
+
+    if (page !== null && page.creator.toString() !== user._id.toString()) {
+      await this.crowi.pageService.deleteCompletelyUserHomeBySystem(user, userHomePagePath);
+      page = null;
+    }
+
+    if (page == null) {
+      const body = `# ${user.username}\nThis is ${user.username}'s page`;
+
+      try {
+        await this.crowi.pageService.create(userHomePagePath, body, user, {});
+        logger.debug('User page created', page);
+      }
+      catch (err) {
+        logger.error('Failed to create user page', err);
+      }
+    }
+  }
+
+}
+
+export default UserEvent;

+ 1 - 1
apps/app/src/server/models/config.ts

@@ -67,7 +67,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'security:pageRecursiveDeletionAuthority' : undefined,
   'security:pageRecursiveCompleteDeletionAuthority' : undefined,
   'security:disableLinkSharing' : false,
-  'security:isUserPageDeletionEnabled': false,
+  'security:isUsersHomePageDeletionEnabled': false,
 
   'security:passport-local:isEnabled' : true,
   'security:passport-ldap:isEnabled' : false,

+ 0 - 4
apps/app/src/server/models/obsolete-page.js

@@ -287,10 +287,6 @@ export const getPageSchema = (crowi) => {
       });
   };
 
-  pageSchema.statics.getUserPagePath = function(user) {
-    return `/user/${user.username}`;
-  };
-
   pageSchema.statics.getDeletedPageName = function(path) {
     if (path.match('/')) {
       // eslint-disable-next-line no-param-reassign

+ 0 - 17
apps/app/src/server/models/page.ts

@@ -958,23 +958,6 @@ schema.statics.findNonEmptyClosestAncestor = async function(path: string): Promi
   return ancestors[0];
 };
 
-schema.statics.removeUserHome = async function(
-    username: string,
-): Promise<{ deleteManyResult: DeleteResult, findOneAndRemoveResult: PageDocument & HasObjectId | null }> {
-  const userHomePagePath = `/user/${username}`;
-
-  // https://regex101.com/r/PY1tI5/1
-  const regex = new RegExp(`^${userHomePagePath}/.+`);
-
-  const [deleteManyResult, findOneAndRemoveResult] = await Promise.all([
-    this.deleteMany({ path: regex }),
-    this.findOneAndRemove({ path: userHomePagePath }),
-  ]);
-
-  return { deleteManyResult, findOneAndRemoveResult };
-};
-
-
 export type PageCreateOptions = {
   format?: string
   grantUserGroupId?: ObjectIdLike

+ 4 - 4
apps/app/src/server/routes/apiv3/security-setting.js

@@ -27,7 +27,7 @@ const validator = {
     body('pageCompleteDeletionAuthority').if(value => value != null).isString().isIn(Object.values(PageDeleteConfigValue)),
     body('hideRestrictedByOwner').if(value => value != null).isBoolean(),
     body('hideRestrictedByGroup').if(value => value != null).isBoolean(),
-    body('isUserPageDeletionEnabled').if(value => value != null).isBoolean(),
+    body('isUsersHomePageDeletionEnabled').if(value => value != null).isBoolean(),
   ],
   shareLinkSetting: [
     body('disableLinkSharing').if(value => value != null).isBoolean(),
@@ -355,7 +355,7 @@ module.exports = (crowi) => {
         pageRecursiveCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
-        isUserPageDeletionEnabled: await crowi.configManager.getConfig('crowi', 'security:isUserPageDeletionEnabled'),
+        isUsersHomePageDeletionEnabled: await crowi.configManager.getConfig('crowi', 'security:isUsersHomePageDeletionEnabled'),
         wikiMode: await crowi.configManager.getConfig('crowi', 'security:wikiMode'),
         sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
       },
@@ -614,7 +614,7 @@ module.exports = (crowi) => {
       'security:pageRecursiveCompleteDeletionAuthority': req.body.pageRecursiveCompleteDeletionAuthority,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
       'security:list-policy:hideRestrictedByGroup': req.body.hideRestrictedByGroup,
-      'security:isUserPageDeletionEnabled': req.body.isUserPageDeletionEnabled,
+      'security:isUsersHomePageDeletionEnabled': req.body.isUsersHomePageDeletionEnabled,
     };
 
     // Validate delete config
@@ -643,7 +643,7 @@ module.exports = (crowi) => {
         pageRecursiveCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority'),
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
-        isUserPageDeletionEnabled: await crowi.configManager.getConfig('crowi', 'security:isUserPageDeletionEnabled'),
+        isUsersHomePageDeletionEnabled: await crowi.configManager.getConfig('crowi', 'security:isUsersHomePageDeletionEnabled'),
       };
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_SECURITY_SETTINGS_UPDATE };

+ 28 - 15
apps/app/src/server/routes/apiv3/users.js

@@ -6,7 +6,7 @@ import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-
+import { configManager } from '../../service/config-manager';
 
 const logger = loggerFactory('growi:routes:apiv3:users');
 
@@ -351,7 +351,7 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3('find-user-is-not-found'));
     }
 
-    const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
+    const limit = parseInt(req.query.limit) || await configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
     const page = req.query.page;
     const offset = (page - 1) * limit;
     const queryOptions = { offset, limit };
@@ -747,6 +747,7 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(err));
     }
   });
+
   /**
    * @swagger
    *
@@ -756,7 +757,7 @@ module.exports = (crowi) => {
    *        tags: [Users]
    *        operationId: removeUser
    *        summary: /users/{id}/remove
-   *        description: Delete user
+   *        description: Delete user and if isUsersHomePageDeletionEnabled delete user home page and subpages
    *        parameters:
    *          - name: id
    *            in: path
@@ -766,32 +767,44 @@ module.exports = (crowi) => {
    *              type: string
    *        responses:
    *          200:
-   *            description: Deleting user success
+   *            description: Deleting user success and if isUsersHomePageDeletionEnabled delete user home page and subpages success
    *            content:
    *              application/json:
    *                schema:
    *                  properties:
-   *                    userData:
+   *                    user:
    *                      type: object
-   *                      description: data of delete user
+   *                      description: data of deleted user
+   *                    userHomePagePath:
+   *                      type: string
+   *                      description: a user home page path
+   *                    isUsersHomePageDeletionEnabled:
+   *                      type: boolean
+   *                      description: is users home page deletion enabled
    */
   router.delete('/:id/remove', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
     const { id } = req.params;
-    const isUserPageDeletionEnabled = crowi.configManager.getConfig('crowi', 'security:isUserPageDeletionEnabled');
+    const isUsersHomePageDeletionEnabled = configManager.getConfig('crowi', 'security:isUsersHomePageDeletionEnabled');
 
     try {
-      const userData = await User.findById(id);
-      const username = userData.username;
-      await UserGroupRelation.remove({ relatedUser: userData });
-      await userData.statusDelete();
-      await ExternalAccount.remove({ user: userData });
-      if (isUserPageDeletionEnabled) await Page.removeUserHome(username);
+      const user = await User.findById(id);
+      // !! DO NOT MOVE userHomePagePath FROM THIS POSITION !! -- 05.31.2023
+      // catch username before delete user because username will be change to deleted_at_*
+      const userHomePagePath = `/user/${user.username}`;
 
-      const serializedUserData = serializeUserSecurely(userData);
+      await UserGroupRelation.remove({ relatedUser: user });
+      await user.statusDelete();
+      await ExternalAccount.remove({ user });
+
+      const serializedUser = serializeUserSecurely(user);
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
 
-      return res.apiv3({ userData: serializedUserData });
+      if (isUsersHomePageDeletionEnabled) {
+        crowi.pageService.deleteCompletelyUserHomeBySystem(req.user, userHomePagePath);
+      }
+
+      return res.apiv3({ user: serializedUser });
     }
     catch (err) {
       logger.error('Error', err);

+ 58 - 0
apps/app/src/server/service/page.ts

@@ -1962,6 +1962,64 @@ class PageService {
     }
   }
 
+  // TODO: Delete user arg.
+  // see: https://redmine.weseek.co.jp/issues/124326
+  /**
+   * @description This function is intended to be used exclusively for forcibly deleting the user homepage by the system.
+   * It should only be called from within the appropriate context and with caution as it performs a system-level operation.
+   *
+   * @param {object} user - The user object.
+   * @param {string} userHomePagePath - The path of the user's homepage.
+   * @returns {Promise<void>} - A Promise that resolves when the deletion is complete.
+   * @throws {Error} - If an error occurs during the deletion process.
+   */
+  async deleteCompletelyUserHomeBySystem(user: object, userHomePagePath: string): Promise<void> {
+    const Page = this.crowi.model('Page');
+    const userHomePage = await Page.findByPath(userHomePagePath, user);
+    const options = {};
+
+    if (userHomePage == null) {
+      logger.error('user home page is not found.');
+      return;
+    }
+
+    const ids = [userHomePage._id];
+    const paths = [userHomePage.path];
+
+    let pageOp;
+    try {
+      // 1. update descendantCount
+      const inc = userHomePage.isEmpty ? -userHomePage.descendantCount : -(userHomePage.descendantCount + 1);
+      await this.updateDescendantCountOfAncestors(userHomePage.parent, inc, true);
+      // 2. delete target completely
+      await this.deleteCompletelyOperation(ids, paths);
+      // 3. delete leaf empty pages
+      await Page.removeLeafEmptyPagesRecursively(userHomePage.parent);
+
+      if (!userHomePage.isEmpty) {
+        this.pageEvent.emit('deleteCompletely', userHomePage, user);
+      }
+
+      pageOp = await PageOperation.create({
+        actionType: PageActionType.DeleteCompletely,
+        actionStage: PageActionStage.Main,
+        page: userHomePage,
+        user,
+        fromPath: userHomePage.path,
+        options,
+      });
+
+      await this.deleteCompletelyRecursivelyMainOperation(userHomePage, user, options, pageOp._id);
+    }
+    catch (err) {
+      logger.error('Error occurred while deleting user home page and subpages.', err);
+      if (pageOp != null) {
+        await PageOperation.deleteOne({ _id: pageOp._id });
+      }
+      throw err;
+    }
+  }
+
   // use the same process in both v4 and v5
   private async revertDeletedDescendants(pages, user) {
     const Page = this.crowi.model('Page');