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

Merge pull request #10536 from growilabs/support/156162-166092-app-apiv3-routes-biome-3

support: Configure biome for apiv3 routes (remaining ts files)
Yuki Takei 4 месяцев назад
Родитель
Сommit
69ae5c3aa4

+ 1 - 0
apps/app/.eslintrc.js

@@ -71,6 +71,7 @@ module.exports = {
     'src/server/routes/apiv3/security-settings/**',
     'src/server/routes/apiv3/security-settings/**',
     'src/server/routes/apiv3/app-settings/**',
     'src/server/routes/apiv3/app-settings/**',
     'src/server/routes/apiv3/page/**',
     'src/server/routes/apiv3/page/**',
+    'src/server/routes/apiv3/*.ts',
   ],
   ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript

+ 50 - 37
apps/app/src/server/routes/apiv3/activity.ts

@@ -1,11 +1,11 @@
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
-import { parseISO, addMinutes, isValid } from 'date-fns';
+import { addMinutes, isValid, parseISO } from 'date-fns';
 import type { Request, Router } from 'express';
 import type { Request, Router } from 'express';
 import express from 'express';
 import express from 'express';
 import { query } from 'express-validator';
 import { query } from 'express-validator';
 
 
 import type { IActivity, ISearchFilter } from '~/interfaces/activity';
 import type { IActivity, ISearchFilter } from '~/interfaces/activity';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import Activity from '~/server/models/activity';
 import Activity from '~/server/models/activity';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
@@ -13,18 +13,21 @@ import loggerFactory from '~/utils/logger';
 
 
 import type Crowi from '../../crowi';
 import type Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
-
 const logger = loggerFactory('growi:routes:apiv3:activity');
 const logger = loggerFactory('growi:routes:apiv3:activity');
 
 
-
 const validator = {
 const validator = {
   list: [
   list: [
-    query('limit').optional().isInt({ max: 100 }).withMessage('limit must be a number less than or equal to 100'),
+    query('limit')
+      .optional()
+      .isInt({ max: 100 })
+      .withMessage('limit must be a number less than or equal to 100'),
     query('offset').optional().isInt().withMessage('page must be a number'),
     query('offset').optional().isInt().withMessage('page must be a number'),
-    query('searchFilter').optional().isString().withMessage('query must be a string'),
+    query('searchFilter')
+      .optional()
+      .isString()
+      .withMessage('query must be a string'),
   ],
   ],
 };
 };
 
 
@@ -171,7 +174,9 @@ const validator = {
 
 
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(
+    crowi,
+  );
 
 
   const router = express.Router();
   const router = express.Router();
 
 
@@ -209,9 +214,14 @@ module.exports = (crowi: Crowi): Router => {
    *             schema:
    *             schema:
    *               $ref: '#/components/schemas/ActivityResponse'
    *               $ref: '#/components/schemas/ActivityResponse'
    */
    */
-  router.get('/',
+  router.get(
+    '/',
     accessTokenParser([SCOPE.READ.ADMIN.AUDIT_LOG], { acceptLegacy: true }),
     accessTokenParser([SCOPE.READ.ADMIN.AUDIT_LOG], { acceptLegacy: true }),
-    loginRequiredStrictly, adminRequired, validator.list, apiV3FormValidator, async(req: Request, res: ApiV3Response) => {
+    loginRequiredStrictly,
+    adminRequired,
+    validator.list,
+    apiV3FormValidator,
+    async (req: Request, res: ApiV3Response) => {
       const auditLogEnabled = configManager.getConfig('app:auditLogEnabled');
       const auditLogEnabled = configManager.getConfig('app:auditLogEnabled');
       if (!auditLogEnabled) {
       if (!auditLogEnabled) {
         const msg = 'AuditLog is not enabled';
         const msg = 'AuditLog is not enabled';
@@ -219,28 +229,36 @@ module.exports = (crowi: Crowi): Router => {
         return res.apiv3Err(msg, 405);
         return res.apiv3Err(msg, 405);
       }
       }
 
 
-      const limit = req.query.limit || configManager.getConfig('customize:showPageLimitationS');
+      const limit =
+        req.query.limit ||
+        configManager.getConfig('customize:showPageLimitationS');
       const offset = req.query.offset || 1;
       const offset = req.query.offset || 1;
 
 
       const query = {};
       const query = {};
 
 
       try {
       try {
-        const parsedSearchFilter = JSON.parse(req.query.searchFilter as string) as ISearchFilter;
+        const parsedSearchFilter = JSON.parse(
+          req.query.searchFilter as string,
+        ) as ISearchFilter;
 
 
         // add username to query
         // add username to query
-        const canContainUsernameFilterToQuery = (
-          parsedSearchFilter.usernames != null
-        && parsedSearchFilter.usernames.length > 0
-        && parsedSearchFilter.usernames.every(u => typeof u === 'string')
-        );
+        const canContainUsernameFilterToQuery =
+          parsedSearchFilter.usernames != null &&
+          parsedSearchFilter.usernames.length > 0 &&
+          parsedSearchFilter.usernames.every((u) => typeof u === 'string');
         if (canContainUsernameFilterToQuery) {
         if (canContainUsernameFilterToQuery) {
-          Object.assign(query, { 'snapshot.username': parsedSearchFilter.usernames });
+          Object.assign(query, {
+            'snapshot.username': parsedSearchFilter.usernames,
+          });
         }
         }
 
 
         // add action to query
         // add action to query
         if (parsedSearchFilter.actions != null) {
         if (parsedSearchFilter.actions != null) {
-          const availableActions = crowi.activityService.getAvailableActions(false);
-          const searchableActions = parsedSearchFilter.actions.filter(action => availableActions.includes(action));
+          const availableActions =
+            crowi.activityService.getAvailableActions(false);
+          const searchableActions = parsedSearchFilter.actions.filter(
+            (action) => availableActions.includes(action),
+          );
           Object.assign(query, { action: searchableActions });
           Object.assign(query, { action: searchableActions });
         }
         }
 
 
@@ -255,8 +273,7 @@ module.exports = (crowi: Crowi): Router => {
               $lt: addMinutes(endDate, 1439),
               $lt: addMinutes(endDate, 1439),
             },
             },
           });
           });
-        }
-        else if (isValid(startDate) && !isValid(endDate)) {
+        } else if (isValid(startDate) && !isValid(endDate)) {
           Object.assign(query, {
           Object.assign(query, {
             createdAt: {
             createdAt: {
               $gte: startDate,
               $gte: startDate,
@@ -265,23 +282,19 @@ module.exports = (crowi: Crowi): Router => {
             },
             },
           });
           });
         }
         }
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Invalid value', err);
         logger.error('Invalid value', err);
         return res.apiv3Err(err, 400);
         return res.apiv3Err(err, 400);
       }
       }
 
 
       try {
       try {
-        const paginateResult = await Activity.paginate(
-          query,
-          {
-            lean: true,
-            limit,
-            offset,
-            sort: { createdAt: -1 },
-            populate: 'user',
-          },
-        );
+        const paginateResult = await Activity.paginate(query, {
+          lean: true,
+          limit,
+          offset,
+          sort: { createdAt: -1 },
+          populate: 'user',
+        });
 
 
         const serializedDocs = paginateResult.docs.map((doc: IActivity) => {
         const serializedDocs = paginateResult.docs.map((doc: IActivity) => {
           const { user, ...rest } = doc;
           const { user, ...rest } = doc;
@@ -297,12 +310,12 @@ module.exports = (crowi: Crowi): Router => {
         };
         };
 
 
         return res.apiv3({ serializedPaginationResult });
         return res.apiv3({ serializedPaginationResult });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Failed to get paginated activity', err);
         logger.error('Failed to get paginated activity', err);
         return res.apiv3Err(err, 500);
         return res.apiv3Err(err, 500);
       }
       }
-    });
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 26 - 15
apps/app/src/server/routes/apiv3/admin-home.ts

@@ -1,4 +1,5 @@
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { SCOPE } from '@growi/core/dist/interfaces';
+
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import { getGrowiVersion } from '~/utils/growi-version';
 import { getGrowiVersion } from '~/utils/growi-version';
@@ -60,7 +61,9 @@ const router = express.Router();
  */
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(
+    crowi,
+  );
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
 
 
   /**
   /**
@@ -83,22 +86,30 @@ module.exports = (crowi) => {
    *                    adminHomeParams:
    *                    adminHomeParams:
    *                      $ref: "#/components/schemas/SystemInformationParams"
    *                      $ref: "#/components/schemas/SystemInformationParams"
    */
    */
-  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.TOP]), loginRequiredStrictly, adminRequired, async(req, res) => {
-    const { getRuntimeVersions } = await import('~/server/util/runtime-versions');
-    const runtimeVersions = await getRuntimeVersions();
+  router.get(
+    '/',
+    accessTokenParser([SCOPE.READ.ADMIN.TOP]),
+    loginRequiredStrictly,
+    adminRequired,
+    async (req, res) => {
+      const { getRuntimeVersions } = await import(
+        '~/server/util/runtime-versions'
+      );
+      const runtimeVersions = await getRuntimeVersions();
 
 
-    const adminHomeParams = {
-      growiVersion: getGrowiVersion(),
-      nodeVersion: runtimeVersions.node ?? '-',
-      npmVersion: runtimeVersions.npm ?? '-',
-      pnpmVersion: runtimeVersions.pnpm ?? '-',
-      envVars: configManager.getManagedEnvVars(),
-      isV5Compatible: configManager.getConfig('app:isV5Compatible'),
-      isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'),
-    };
+      const adminHomeParams = {
+        growiVersion: getGrowiVersion(),
+        nodeVersion: runtimeVersions.node ?? '-',
+        npmVersion: runtimeVersions.npm ?? '-',
+        pnpmVersion: runtimeVersions.pnpm ?? '-',
+        envVars: configManager.getManagedEnvVars(),
+        isV5Compatible: configManager.getConfig('app:isV5Compatible'),
+        isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'),
+      };
 
 
-    return res.apiv3({ adminHomeParams });
-  });
+      return res.apiv3({ adminHomeParams });
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 157 - 94
apps/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -1,9 +1,9 @@
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
 import type { Types } from 'mongoose';
 import type { Types } from 'mongoose';
 
 
 import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
@@ -99,29 +99,44 @@ const router = express.Router();
 const validator = {
 const validator = {
   bookmarkFolder: [
   bookmarkFolder: [
     body('name').isString().withMessage('name must be a string'),
     body('name').isString().withMessage('name must be a string'),
-    body('parent').isMongoId().optional({ nullable: true })
-      .custom(async(parent: string) => {
+    body('parent')
+      .isMongoId()
+      .optional({ nullable: true })
+      .custom(async (parent: string) => {
         const parentFolder = await BookmarkFolder.findById(parent);
         const parentFolder = await BookmarkFolder.findById(parent);
         if (parentFolder == null || parentFolder.parent != null) {
         if (parentFolder == null || parentFolder.parent != null) {
           throw new Error('Maximum folder hierarchy of 2 levels');
           throw new Error('Maximum folder hierarchy of 2 levels');
         }
         }
       }),
       }),
-    body('childFolder').optional().isArray().withMessage('Children must be an array'),
-    body('bookmarkFolderId').optional().isMongoId().withMessage('Bookark Folder ID must be a valid mongo ID'),
+    body('childFolder')
+      .optional()
+      .isArray()
+      .withMessage('Children must be an array'),
+    body('bookmarkFolderId')
+      .optional()
+      .isMongoId()
+      .withMessage('Bookark Folder ID must be a valid mongo ID'),
   ],
   ],
   bookmarkPage: [
   bookmarkPage: [
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
-    body('folderId').optional({ nullable: true }).isMongoId().withMessage('Folder ID must be a valid mongo ID'),
+    body('folderId')
+      .optional({ nullable: true })
+      .isMongoId()
+      .withMessage('Folder ID must be a valid mongo ID'),
   ],
   ],
   bookmark: [
   bookmark: [
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
-    body('status').isBoolean().withMessage('status must be one of true or false'),
+    body('status')
+      .isBoolean()
+      .withMessage('status must be one of true or false'),
   ],
   ],
 };
 };
 
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(
+    crowi,
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -157,28 +172,36 @@ module.exports = (crowi) => {
    *                      type: object
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
    */
-  router.post('/',
+  router.post(
+    '/',
     accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }),
     accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }),
-    loginRequiredStrictly, validator.bookmarkFolder, apiV3FormValidator, async(req, res) => {
+    loginRequiredStrictly,
+    validator.bookmarkFolder,
+    apiV3FormValidator,
+    async (req, res) => {
       const owner = req.user?._id;
       const owner = req.user?._id;
       const { name, parent } = req.body;
       const { name, parent } = req.body;
       const params = {
       const params = {
-        name, owner, parent,
+        name,
+        owner,
+        parent,
       };
       };
 
 
       try {
       try {
         const bookmarkFolder = await BookmarkFolder.createByParameters(params);
         const bookmarkFolder = await BookmarkFolder.createByParameters(params);
         logger.debug('bookmark folder created', bookmarkFolder);
         logger.debug('bookmark folder created', bookmarkFolder);
         return res.apiv3({ bookmarkFolder });
         return res.apiv3({ bookmarkFolder });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
         if (err instanceof InvalidParentBookmarkFolderError) {
         if (err instanceof InvalidParentBookmarkFolderError) {
-          return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_bookmark_folder'));
+          return res.apiv3Err(
+            new ErrorV3(err.message, 'failed_to_create_bookmark_folder'),
+          );
         }
         }
         return res.apiv3Err(err, 500);
         return res.apiv3Err(err, 500);
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -211,63 +234,75 @@ module.exports = (crowi) => {
    *                        type: object
    *                        type: object
    *                        $ref: '#/components/schemas/BookmarkFolder'
    *                        $ref: '#/components/schemas/BookmarkFolder'
    */
    */
-  router.get('/list/:userId', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => {
-    const { userId } = req.params;
+  router.get(
+    '/list/:userId',
+    accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK], { acceptLegacy: true }),
+    loginRequiredStrictly,
+    async (req, res) => {
+      const { userId } = req.params;
 
 
-    const getBookmarkFolders = async(
+      const getBookmarkFolders = async (
         userId: Types.ObjectId | string,
         userId: Types.ObjectId | string,
         parentFolderId?: Types.ObjectId | string,
         parentFolderId?: Types.ObjectId | string,
-    ) => {
-      const folders = await BookmarkFolder.find({ owner: userId, parent: parentFolderId })
-        .populate('childFolder')
-        .populate({
-          path: 'bookmarks',
-          model: 'Bookmark',
-          populate: {
-            path: 'page',
-            model: 'Page',
+      ) => {
+        const folders = (await BookmarkFolder.find({
+          owner: userId,
+          parent: parentFolderId,
+        })
+          .populate('childFolder')
+          .populate({
+            path: 'bookmarks',
+            model: 'Bookmark',
             populate: {
             populate: {
-              path: 'lastUpdateUser',
-              model: 'User',
+              path: 'page',
+              model: 'Page',
+              populate: {
+                path: 'lastUpdateUser',
+                model: 'User',
+              },
             },
             },
-          },
-        }).exec() as never as BookmarkFolderItems[];
+          })
+          .exec()) as never as BookmarkFolderItems[];
 
 
-      const returnValue: BookmarkFolderItems[] = [];
+        const returnValue: BookmarkFolderItems[] = [];
 
 
-      const promises = folders.map(async(folder: BookmarkFolderItems) => {
-        const childFolder = await getBookmarkFolders(userId, folder._id);
+        const promises = folders.map(async (folder: BookmarkFolderItems) => {
+          const childFolder = await getBookmarkFolders(userId, folder._id);
 
 
-        // !! DO NOT THIS SERIALIZING OUTSIDE OF PROMISES !! -- 05.23.2023 ryoji-s
-        // Serializing outside of promises will cause not populated.
-        const bookmarks = folder.bookmarks.map(bookmark => serializeBookmarkSecurely(bookmark));
+          // !! DO NOT THIS SERIALIZING OUTSIDE OF PROMISES !! -- 05.23.2023 ryoji-s
+          // Serializing outside of promises will cause not populated.
+          const bookmarks = folder.bookmarks.map((bookmark) =>
+            serializeBookmarkSecurely(bookmark),
+          );
 
 
-        const res = {
-          _id: folder._id.toString(),
-          name: folder.name,
-          owner: folder.owner,
-          bookmarks,
-          childFolder,
-          parent: folder.parent,
-        };
-        return res;
-      });
+          const res = {
+            _id: folder._id.toString(),
+            name: folder.name,
+            owner: folder.owner,
+            bookmarks,
+            childFolder,
+            parent: folder.parent,
+          };
+          return res;
+        });
 
 
-      const results = await Promise.all(promises) as unknown as BookmarkFolderItems[];
-      returnValue.push(...results);
-      return returnValue;
-    };
+        const results = (await Promise.all(
+          promises,
+        )) as unknown as BookmarkFolderItems[];
+        returnValue.push(...results);
+        return returnValue;
+      };
 
 
-    try {
-      const bookmarkFolderItems = await getBookmarkFolders(userId, undefined);
+      try {
+        const bookmarkFolderItems = await getBookmarkFolders(userId, undefined);
 
 
-      return res.apiv3({ bookmarkFolderItems });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+        return res.apiv3({ bookmarkFolderItems });
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err, 500);
+      }
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -299,18 +334,22 @@ module.exports = (crowi) => {
    *                      description: Number of deleted folders
    *                      description: Number of deleted folders
    *                      example: 1
    *                      example: 1
    */
    */
-  router.delete('/:id', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => {
-    const { id } = req.params;
-    try {
-      const result = await BookmarkFolder.deleteFolderAndChildren(id);
-      const { deletedCount } = result;
-      return res.apiv3({ deletedCount });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+  router.delete(
+    '/:id',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }),
+    loginRequiredStrictly,
+    async (req, res) => {
+      const { id } = req.params;
+      try {
+        const result = await BookmarkFolder.deleteFolderAndChildren(id);
+        const { deletedCount } = result;
+        return res.apiv3({ deletedCount });
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err, 500);
+      }
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -355,20 +394,27 @@ module.exports = (crowi) => {
    *                      type: object
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
    */
-  router.put('/',
-    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
-      const {
-        bookmarkFolderId, name, parent, childFolder,
-      } = req.body;
+  router.put(
+    '/',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }),
+    loginRequiredStrictly,
+    validator.bookmarkFolder,
+    async (req, res) => {
+      const { bookmarkFolderId, name, parent, childFolder } = req.body;
       try {
       try {
-        const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent, childFolder);
+        const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(
+          bookmarkFolderId,
+          name,
+          parent,
+          childFolder,
+        );
         return res.apiv3({ bookmarkFolder });
         return res.apiv3({ bookmarkFolder });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
         return res.apiv3Err(err, 500);
         return res.apiv3Err(err, 500);
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -405,22 +451,31 @@ module.exports = (crowi) => {
    *                      type: object
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
    */
-  router.post('/add-bookmark-to-folder',
-    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmarkPage, apiV3FormValidator,
-    async(req, res) => {
+  router.post(
+    '/add-bookmark-to-folder',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }),
+    loginRequiredStrictly,
+    validator.bookmarkPage,
+    apiV3FormValidator,
+    async (req, res) => {
       const userId = req.user?._id;
       const userId = req.user?._id;
       const { pageId, folderId } = req.body;
       const { pageId, folderId } = req.body;
 
 
       try {
       try {
-        const bookmarkFolder = await BookmarkFolder.insertOrUpdateBookmarkedPage(pageId, userId, folderId);
+        const bookmarkFolder =
+          await BookmarkFolder.insertOrUpdateBookmarkedPage(
+            pageId,
+            userId,
+            folderId,
+          );
         logger.debug('bookmark added to folder', bookmarkFolder);
         logger.debug('bookmark added to folder', bookmarkFolder);
         return res.apiv3({ bookmarkFolder });
         return res.apiv3({ bookmarkFolder });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
         return res.apiv3Err(err, 500);
         return res.apiv3Err(err, 500);
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -456,18 +511,26 @@ module.exports = (crowi) => {
    *                      type: object
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
    */
-  router.put('/update-bookmark',
-    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmark, async(req, res) => {
+  router.put(
+    '/update-bookmark',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }),
+    loginRequiredStrictly,
+    validator.bookmark,
+    async (req, res) => {
       const { pageId, status } = req.body;
       const { pageId, status } = req.body;
       const userId = req.user?._id;
       const userId = req.user?._id;
       try {
       try {
-        const bookmarkFolder = await BookmarkFolder.updateBookmark(pageId, status, userId);
+        const bookmarkFolder = await BookmarkFolder.updateBookmark(
+          pageId,
+          status,
+          userId,
+        );
         return res.apiv3({ bookmarkFolder });
         return res.apiv3({ bookmarkFolder });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
         return res.apiv3Err(err, 500);
         return res.apiv3Err(err, 500);
       }
       }
-    });
+    },
+  );
   return router;
   return router;
 };
 };

+ 315 - 157
apps/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -1,13 +1,13 @@
-import { createReadStream } from 'fs';
-import path from 'path';
-
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { NextFunction, Request, Router } from 'express';
 import type { NextFunction, Request, Router } from 'express';
 import express from 'express';
 import express from 'express';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
+import { createReadStream } from 'fs';
 import multer from 'multer';
 import multer from 'multer';
+import path from 'path';
 
 
+import type { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
 import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
@@ -19,15 +19,13 @@ import { getImportService } from '~/server/service/import';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
 import { TransferKey } from '~/utils/vo/transfer-key';
 
 
-
 import type Crowi from '../../crowi';
 import type Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { Attachment } from '../../models/attachment';
 import { Attachment } from '../../models/attachment';
-
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
 interface AuthorizedRequest extends Request {
 interface AuthorizedRequest extends Request {
-  user?: any
+  user?: any;
 }
 }
 
 
 const logger = loggerFactory('growi:routes:apiv3:transfer');
 const logger = loggerFactory('growi:routes:apiv3:transfer');
@@ -76,20 +74,27 @@ const validator = {
  *                 type: string
  *                 type: string
  *               containerName:
  *               containerName:
  *                 type: string
  *                 type: string
-*/
+ */
 /*
 /*
  * Routes
  * Routes
  */
  */
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
   const {
   const {
-    g2gTransferPusherService, g2gTransferReceiverService,
+    g2gTransferPusherService,
+    g2gTransferReceiverService,
     growiBridgeService,
     growiBridgeService,
   } = crowi;
   } = crowi;
 
 
   const importService = getImportService();
   const importService = getImportService();
 
 
-  if (g2gTransferPusherService == null || g2gTransferReceiverService == null || exportService == null || importService == null
-    || growiBridgeService == null || configManager == null) {
+  if (
+    g2gTransferPusherService == null ||
+    g2gTransferReceiverService == null ||
+    exportService == null ||
+    importService == null ||
+    growiBridgeService == null ||
+    configManager == null
+  ) {
     throw Error('GROWI is not ready for g2g transfer');
     throw Error('GROWI is not ready for g2g transfer');
   }
   }
 
 
@@ -126,10 +131,16 @@ module.exports = (crowi: Crowi): Router => {
   const isInstalled = configManager.getConfig('app:installed');
   const isInstalled = configManager.getConfig('app:installed');
 
 
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(
+    crowi,
+  );
 
 
   // Middleware
   // Middleware
-  const adminRequiredIfInstalled = (req: Request, res: ApiV3Response, next: NextFunction) => {
+  const adminRequiredIfInstalled = (
+    req: Request,
+    res: ApiV3Response,
+    next: NextFunction,
+  ) => {
     if (!isInstalled) {
     if (!isInstalled) {
       next();
       next();
       return;
       return;
@@ -139,29 +150,47 @@ module.exports = (crowi: Crowi): Router => {
   };
   };
 
 
   // Middleware
   // Middleware
-  const appSiteUrlRequiredIfNotInstalled = (req: Request, res: ApiV3Response, next: NextFunction) => {
+  const appSiteUrlRequiredIfNotInstalled = (
+    req: Request,
+    res: ApiV3Response,
+    next: NextFunction,
+  ) => {
     if (!isInstalled && req.body.appSiteUrl != null) {
     if (!isInstalled && req.body.appSiteUrl != null) {
       next();
       next();
       return;
       return;
     }
     }
 
 
-    if (configManager.getConfig('app:siteUrl') != null || req.body.appSiteUrl != null) {
+    if (
+      configManager.getConfig('app:siteUrl') != null ||
+      req.body.appSiteUrl != null
+    ) {
       next();
       next();
       return;
       return;
     }
     }
 
 
-    return res.apiv3Err(new ErrorV3('Body param "appSiteUrl" is required when GROWI is NOT installed yet'), 400);
+    return res.apiv3Err(
+      new ErrorV3(
+        'Body param "appSiteUrl" is required when GROWI is NOT installed yet',
+      ),
+      400,
+    );
   };
   };
 
 
   // Local middleware to check if key is valid or not
   // Local middleware to check if key is valid or not
-  const validateTransferKey = async(req: Request, res: ApiV3Response, next: NextFunction) => {
+  const validateTransferKey = async (
+    req: Request,
+    res: ApiV3Response,
+    next: NextFunction,
+  ) => {
     const transferKey = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME] as string;
     const transferKey = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME] as string;
 
 
     try {
     try {
       await g2gTransferReceiverService.validateTransferKey(transferKey);
       await g2gTransferReceiverService.validateTransferKey(transferKey);
-    }
-    catch (err) {
-      return res.apiv3Err(new ErrorV3('Invalid transfer key', 'invalid_transfer_key'), 403);
+    } catch (err) {
+      return res.apiv3Err(
+        new ErrorV3('Invalid transfer key', 'invalid_transfer_key'),
+        403,
+      );
     }
     }
 
 
     next();
     next();
@@ -200,10 +229,14 @@ module.exports = (crowi: Crowi): Router => {
    *                          type: number
    *                          type: number
    *                          description: The size of the file
    *                          description: The size of the file
    */
    */
-  receiveRouter.get('/files', validateTransferKey, async(req: Request, res: ApiV3Response) => {
-    const files = await crowi.fileUploadService.listFiles();
-    return res.apiv3({ files });
-  });
+  receiveRouter.get(
+    '/files',
+    validateTransferKey,
+    async (req: Request, res: ApiV3Response) => {
+      const files = await crowi.fileUploadService.listFiles();
+      return res.apiv3({ files });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -251,88 +284,122 @@ module.exports = (crowi: Crowi): Router => {
    *                    type: string
    *                    type: string
    *                    description: The message of the result
    *                    description: The message of the result
    */
    */
-  receiveRouter.post('/', validateTransferKey, uploads.single('transferDataZipFile'), async(req: Request & { file: any; }, res: ApiV3Response) => {
-    const { file } = req;
-    const {
-      collections: strCollections,
-      optionsMap: strOptionsMap,
-      operatorUserId,
-      uploadConfigs: strUploadConfigs,
-    } = req.body;
-
-    /*
-     * parse multipart form data
-     */
-    let collections;
-    let optionsMap;
-    let sourceGROWIUploadConfigs;
-    try {
-      collections = JSON.parse(strCollections);
-      optionsMap = JSON.parse(strOptionsMap);
-      sourceGROWIUploadConfigs = JSON.parse(strUploadConfigs);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('Failed to parse request body.', 'parse_failed'), 500);
-    }
+  receiveRouter.post(
+    '/',
+    validateTransferKey,
+    uploads.single('transferDataZipFile'),
+    async (req: Request & { file: any }, res: ApiV3Response) => {
+      const { file } = req;
+      const {
+        collections: strCollections,
+        optionsMap: strOptionsMap,
+        operatorUserId,
+        uploadConfigs: strUploadConfigs,
+      } = req.body;
+
+      /*
+       * parse multipart form data
+       */
+      let collections: string[];
+      let optionsMap: { [key: string]: GrowiArchiveImportOption };
+      let sourceGROWIUploadConfigs: any;
+      try {
+        collections = JSON.parse(strCollections);
+        optionsMap = JSON.parse(strOptionsMap);
+        sourceGROWIUploadConfigs = JSON.parse(strUploadConfigs);
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(
+          new ErrorV3('Failed to parse request body.', 'parse_failed'),
+          500,
+        );
+      }
 
 
-    /*
-     * unzip and parse
-     */
-    let meta;
-    let innerFileStats;
-    try {
-      const zipFile = importService.getFile(file.filename);
-      await importService.unzip(zipFile);
+      /*
+       * unzip and parse
+       */
+      let meta: object | undefined;
+      let innerFileStats: {
+        fileName: string;
+        collectionName: string;
+        size: number;
+      }[];
+      try {
+        const zipFile = importService.getFile(file.filename);
+        await importService.unzip(zipFile);
 
 
-      const zipFileStat = await growiBridgeService.parseZipFile(zipFile);
-      innerFileStats = zipFileStat?.innerFileStats;
-      meta = zipFileStat?.meta;
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500);
-    }
+        const zipFileStat = await growiBridgeService.parseZipFile(zipFile);
+        innerFileStats = zipFileStat?.innerFileStats ?? [];
+        meta = zipFileStat?.meta;
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(
+          new ErrorV3(
+            'Failed to validate transfer data file.',
+            'validation_failed',
+          ),
+          500,
+        );
+      }
 
 
-    /*
-     * validate meta.json
-     */
-    try {
-      importService.validate(meta);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(
-        new ErrorV3(
-          'The version of this GROWI and the uploaded GROWI data are not the same',
-          'version_incompatible',
-        ),
-        500,
-      );
-    }
+      /*
+       * validate meta.json
+       */
+      try {
+        importService.validate(meta);
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(
+          new ErrorV3(
+            'The version of this GROWI and the uploaded GROWI data are not the same',
+            'version_incompatible',
+          ),
+          500,
+        );
+      }
 
 
-    /*
-     * generate maps of ImportSettings to import
-     */
-    let importSettingsMap: Map<string, ImportSettings>;
-    try {
-      importSettingsMap = g2gTransferReceiverService.getImportSettingMap(innerFileStats, optionsMap, operatorUserId);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('Import settings are invalid. See GROWI docs about details.', 'import_settings_invalid'));
-    }
+      /*
+       * generate maps of ImportSettings to import
+       */
+      let importSettingsMap: Map<string, ImportSettings>;
+      try {
+        importSettingsMap = g2gTransferReceiverService.getImportSettingMap(
+          innerFileStats,
+          optionsMap,
+          operatorUserId,
+        );
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(
+          new ErrorV3(
+            'Import settings are invalid. See GROWI docs about details.',
+            'import_settings_invalid',
+          ),
+        );
+      }
 
 
-    try {
-      await g2gTransferReceiverService.importCollections(collections, importSettingsMap, sourceGROWIUploadConfigs);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('Failed to import MongoDB collections', 'mongo_collection_import_failure'), 500);
-    }
+      try {
+        await g2gTransferReceiverService.importCollections(
+          collections,
+          importSettingsMap,
+          sourceGROWIUploadConfigs,
+        );
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(
+          new ErrorV3(
+            'Failed to import MongoDB collections',
+            'mongo_collection_import_failure',
+          ),
+          500,
+        );
+      }
 
 
-    return res.apiv3({ message: 'Successfully started to receive transfer data.' });
-  });
+      return res.apiv3({
+        message: 'Successfully started to receive transfer data.',
+      });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -370,54 +437,101 @@ module.exports = (crowi: Crowi): Router => {
    *                    description: The message of the result
    *                    description: The message of the result
    */
    */
   // This endpoint uses multer's MemoryStorage since the received data should be persisted directly on attachment storage.
   // This endpoint uses multer's MemoryStorage since the received data should be persisted directly on attachment storage.
-  receiveRouter.post('/attachment', validateTransferKey, uploadsForAttachment.single('content'),
-    async(req: Request & { file: any; }, res: ApiV3Response) => {
+  receiveRouter.post(
+    '/attachment',
+    validateTransferKey,
+    uploadsForAttachment.single('content'),
+    async (req: Request & { file: any }, res: ApiV3Response) => {
       const { file } = req;
       const { file } = req;
       const { attachmentMetadata } = req.body;
       const { attachmentMetadata } = req.body;
 
 
-      let attachmentMap;
+      let attachmentMap: { fileName: any; fileSize: any };
       try {
       try {
         attachmentMap = JSON.parse(attachmentMetadata);
         attachmentMap = JSON.parse(attachmentMetadata);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.apiv3Err(new ErrorV3('Failed to parse body.', 'parse_failed'), 500);
+        return res.apiv3Err(
+          new ErrorV3('Failed to parse body.', 'parse_failed'),
+          500,
+        );
       }
       }
 
 
       try {
       try {
         const { fileName, fileSize } = attachmentMap;
         const { fileName, fileSize } = attachmentMap;
-        if (typeof fileName !== 'string' || fileName.length === 0 || fileName.length > 256) {
+        if (
+          typeof fileName !== 'string' ||
+          fileName.length === 0 ||
+          fileName.length > 256
+        ) {
           logger.warn('Invalid fileName in attachment metadata.', { fileName });
           logger.warn('Invalid fileName in attachment metadata.', { fileName });
-          return res.apiv3Err(new ErrorV3('Invalid fileName in attachment metadata.', 'invalid_metadata'), 400);
+          return res.apiv3Err(
+            new ErrorV3(
+              'Invalid fileName in attachment metadata.',
+              'invalid_metadata',
+            ),
+            400,
+          );
         }
         }
-        if (typeof fileSize !== 'number' || !Number.isInteger(fileSize) || fileSize < 0) {
+        if (
+          typeof fileSize !== 'number' ||
+          !Number.isInteger(fileSize) ||
+          fileSize < 0
+        ) {
           logger.warn('Invalid fileSize in attachment metadata.', { fileSize });
           logger.warn('Invalid fileSize in attachment metadata.', { fileSize });
-          return res.apiv3Err(new ErrorV3('Invalid fileSize in attachment metadata.', 'invalid_metadata'), 400);
+          return res.apiv3Err(
+            new ErrorV3(
+              'Invalid fileSize in attachment metadata.',
+              'invalid_metadata',
+            ),
+            400,
+          );
         }
         }
         const count = await Attachment.countDocuments({ fileName, fileSize });
         const count = await Attachment.countDocuments({ fileName, fileSize });
         if (count === 0) {
         if (count === 0) {
-          logger.warn('Attachment not found in collection.', { fileName, fileSize });
-          return res.apiv3Err(new ErrorV3('Attachment not found in collection.', 'attachment_not_found'), 404);
+          logger.warn('Attachment not found in collection.', {
+            fileName,
+            fileSize,
+          });
+          return res.apiv3Err(
+            new ErrorV3(
+              'Attachment not found in collection.',
+              'attachment_not_found',
+            ),
+            404,
+          );
         }
         }
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.apiv3Err(new ErrorV3('Failed to check attachment existence.', 'attachment_check_failed'), 500);
+        return res.apiv3Err(
+          new ErrorV3(
+            'Failed to check attachment existence.',
+            'attachment_check_failed',
+          ),
+          500,
+        );
       }
       }
 
 
       const fileStream = createReadStream(file.path, {
       const fileStream = createReadStream(file.path, {
-        flags: 'r', mode: 0o666, autoClose: true,
+        flags: 'r',
+        mode: 0o666,
+        autoClose: true,
       });
       });
       try {
       try {
-        await g2gTransferReceiverService.receiveAttachment(fileStream, attachmentMap);
-      }
-      catch (err) {
+        await g2gTransferReceiverService.receiveAttachment(
+          fileStream,
+          attachmentMap,
+        );
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.apiv3Err(new ErrorV3('Failed to upload.', 'upload_failed'), 500);
+        return res.apiv3Err(
+          new ErrorV3('Failed to upload.', 'upload_failed'),
+          500,
+        );
       }
       }
 
 
       return res.apiv3({ message: 'Successfully imported attached file.' });
       return res.apiv3({ message: 'Successfully imported attached file.' });
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -439,23 +553,32 @@ module.exports = (crowi: Crowi): Router => {
    *                  growiInfo:
    *                  growiInfo:
    *                    $ref: '#/components/schemas/GrowiInfo'
    *                    $ref: '#/components/schemas/GrowiInfo'
    */
    */
-  receiveRouter.get('/growi-info', validateTransferKey, async(req: Request, res: ApiV3Response) => {
-    let growiInfo: IDataGROWIInfo;
-    try {
-      growiInfo = await g2gTransferReceiverService.answerGROWIInfo();
-    }
-    catch (err) {
-      logger.error(err);
+  receiveRouter.get(
+    '/growi-info',
+    validateTransferKey,
+    async (req: Request, res: ApiV3Response) => {
+      let growiInfo: IDataGROWIInfo;
+      try {
+        growiInfo = await g2gTransferReceiverService.answerGROWIInfo();
+      } catch (err) {
+        logger.error(err);
 
 
-      if (!isG2GTransferError(err)) {
-        return res.apiv3Err(new ErrorV3('Failed to prepare GROWI info', 'failed_to_prepare_growi_info'), 500);
-      }
+        if (!isG2GTransferError(err)) {
+          return res.apiv3Err(
+            new ErrorV3(
+              'Failed to prepare GROWI info',
+              'failed_to_prepare_growi_info',
+            ),
+            500,
+          );
+        }
 
 
-      return res.apiv3Err(new ErrorV3(err.message, err.code), 500);
-    }
+        return res.apiv3Err(new ErrorV3(err.message, err.code), 500);
+      }
 
 
-    return res.apiv3({ growiInfo });
-  });
+      return res.apiv3({ growiInfo });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -489,32 +612,46 @@ module.exports = (crowi: Crowi): Router => {
    *                    type: string
    *                    type: string
    *                    description: The transfer key
    *                    description: The transfer key
    */
    */
-  receiveRouter.post('/generate-key',
+  receiveRouter.post(
+    '/generate-key',
     accessTokenParser([SCOPE.WRITE.ADMIN.EXPORT_DATA], { acceptLegacy: true }),
     accessTokenParser([SCOPE.WRITE.ADMIN.EXPORT_DATA], { acceptLegacy: true }),
-    adminRequiredIfInstalled, appSiteUrlRequiredIfNotInstalled, async(req: Request, res: ApiV3Response) => {
-      const appSiteUrl = req.body.appSiteUrl ?? configManager.getConfig('app:siteUrl');
+    adminRequiredIfInstalled,
+    appSiteUrlRequiredIfNotInstalled,
+    async (req: Request, res: ApiV3Response) => {
+      const appSiteUrl =
+        req.body.appSiteUrl ?? configManager.getConfig('app:siteUrl');
 
 
       let appSiteUrlOrigin: string;
       let appSiteUrlOrigin: string;
       try {
       try {
         appSiteUrlOrigin = new URL(appSiteUrl).origin;
         appSiteUrlOrigin = new URL(appSiteUrl).origin;
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.apiv3Err(new ErrorV3('appSiteUrl may be wrong', 'failed_to_generate_key_string'));
+        return res.apiv3Err(
+          new ErrorV3(
+            'appSiteUrl may be wrong',
+            'failed_to_generate_key_string',
+          ),
+        );
       }
       }
 
 
       // Save TransferKey document
       // Save TransferKey document
       let transferKeyString: string;
       let transferKeyString: string;
       try {
       try {
-        transferKeyString = await g2gTransferReceiverService.createTransferKey(appSiteUrlOrigin);
-      }
-      catch (err) {
+        transferKeyString =
+          await g2gTransferReceiverService.createTransferKey(appSiteUrlOrigin);
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.apiv3Err(new ErrorV3('Error occurred while generating transfer key.', 'failed_to_generate_key'));
+        return res.apiv3Err(
+          new ErrorV3(
+            'Error occurred while generating transfer key.',
+            'failed_to_generate_key',
+          ),
+        );
       }
       }
 
 
       return res.apiv3({ transferKey: transferKeyString });
       return res.apiv3({ transferKey: transferKeyString });
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -556,44 +693,65 @@ module.exports = (crowi: Crowi): Router => {
    *                    type: string
    *                    type: string
    *                    description: The message of the result
    *                    description: The message of the result
    */
    */
-  pushRouter.post('/transfer',
+  pushRouter.post(
+    '/transfer',
     accessTokenParser([SCOPE.WRITE.ADMIN.EXPORT_DATA], { acceptLegacy: true }),
     accessTokenParser([SCOPE.WRITE.ADMIN.EXPORT_DATA], { acceptLegacy: true }),
-    loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    loginRequiredStrictly,
+    adminRequired,
+    validator.transfer,
+    apiV3FormValidator,
+    async (req: AuthorizedRequest, res: ApiV3Response) => {
       const { transferKey, collections, optionsMap } = req.body;
       const { transferKey, collections, optionsMap } = req.body;
 
 
       // Parse transfer key
       // Parse transfer key
       let tk: TransferKey;
       let tk: TransferKey;
       try {
       try {
         tk = TransferKey.parse(transferKey);
         tk = TransferKey.parse(transferKey);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400);
+        return res.apiv3Err(
+          new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'),
+          400,
+        );
       }
       }
 
 
       // get growi info
       // get growi info
       let destGROWIInfo: IDataGROWIInfo;
       let destGROWIInfo: IDataGROWIInfo;
       try {
       try {
         destGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
         destGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         logger.error(err);
-        return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI info.', 'failed_to_ask_growi_info'));
+        return res.apiv3Err(
+          new ErrorV3(
+            'Error occurred while asking GROWI info.',
+            'failed_to_ask_growi_info',
+          ),
+        );
       }
       }
 
 
       // Check if can transfer
       // Check if can transfer
-      const transferability = await g2gTransferPusherService.getTransferability(destGROWIInfo);
+      const transferability =
+        await g2gTransferPusherService.getTransferability(destGROWIInfo);
       if (!transferability.canTransfer) {
       if (!transferability.canTransfer) {
-        return res.apiv3Err(new ErrorV3(transferability.reason, 'growi_incompatible_to_transfer'));
+        return res.apiv3Err(
+          new ErrorV3(transferability.reason, 'growi_incompatible_to_transfer'),
+        );
       }
       }
 
 
       // Start transfer
       // Start transfer
       // DO NOT "await". Let it run in the background.
       // DO NOT "await". Let it run in the background.
       // Errors should be emitted through websocket.
       // Errors should be emitted through websocket.
-      g2gTransferPusherService.startTransfer(tk, req.user, collections, optionsMap, destGROWIInfo);
+      g2gTransferPusherService.startTransfer(
+        tk,
+        req.user,
+        collections,
+        optionsMap,
+        destGROWIInfo,
+      );
 
 
       return res.apiv3({ message: 'Successfully requested auto transfer.' });
       return res.apiv3({ message: 'Successfully requested auto transfer.' });
-    });
+    },
+  );
 
 
   // Merge receiveRouter and pushRouter
   // Merge receiveRouter and pushRouter
   router.use(receiveRouter, pushRouter);
   router.use(receiveRouter, pushRouter);

+ 26 - 13
apps/app/src/server/routes/apiv3/healthcheck.ts

@@ -5,10 +5,8 @@ import nocache from 'nocache';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { Config } from '../../models/config';
 import { Config } from '../../models/config';
-
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
-
 const logger = loggerFactory('growi:routes:apiv3:healthcheck');
 const logger = loggerFactory('growi:routes:apiv3:healthcheck');
 
 
 const router = express.Router();
 const router = express.Router();
@@ -79,15 +77,19 @@ const router = express.Router();
  */
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-
   async function checkMongo(errors, info) {
   async function checkMongo(errors, info) {
     try {
     try {
       await Config.findOne({});
       await Config.findOne({});
 
 
       info.mongo = 'OK';
       info.mongo = 'OK';
-    }
-    catch (err) {
-      errors.push(new ErrorV3(`MongoDB is not connectable - ${err.message}`, 'healthcheck-mongodb-unhealthy', err.stack));
+    } catch (err) {
+      errors.push(
+        new ErrorV3(
+          `MongoDB is not connectable - ${err.message}`,
+          'healthcheck-mongodb-unhealthy',
+          err.stack,
+        ),
+      );
     }
     }
   }
   }
 
 
@@ -97,9 +99,14 @@ module.exports = (crowi) => {
       try {
       try {
         info.searchInfo = await searchService.getInfoForHealth();
         info.searchInfo = await searchService.getInfoForHealth();
         searchService.resetErrorStatus();
         searchService.resetErrorStatus();
-      }
-      catch (err) {
-        errors.push(new ErrorV3(`The Search Service is not connectable - ${err.message}`, 'healthcheck-search-unhealthy', err.stack));
+      } catch (err) {
+        errors.push(
+          new ErrorV3(
+            `The Search Service is not connectable - ${err.message}`,
+            'healthcheck-search-unhealthy',
+            err.stack,
+          ),
+        );
       }
       }
     }
     }
   }
   }
@@ -165,20 +172,26 @@ module.exports = (crowi) => {
    *                  info:
    *                  info:
    *                    $ref: '#/components/schemas/HealthcheckInfo'
    *                    $ref: '#/components/schemas/HealthcheckInfo'
    */
    */
-  router.get('/', nocache(), async(req, res: ApiV3Response) => {
+  router.get('/', nocache(), async (req, res: ApiV3Response) => {
     let checkServices = (() => {
     let checkServices = (() => {
       if (req.query.checkServices == null) return [];
       if (req.query.checkServices == null) return [];
-      return Array.isArray(req.query.checkServices) ? req.query.checkServices : [req.query.checkServices];
+      return Array.isArray(req.query.checkServices)
+        ? req.query.checkServices
+        : [req.query.checkServices];
     })();
     })();
     let isStrictly = req.query.strictly != null;
     let isStrictly = req.query.strictly != null;
 
 
     // for backward compatibility
     // for backward compatibility
     if (req.query.connectToMiddlewares != null) {
     if (req.query.connectToMiddlewares != null) {
-      logger.warn('The param \'connectToMiddlewares\' is deprecated. Use \'checkServices[]\' instead.');
+      logger.warn(
+        "The param 'connectToMiddlewares' is deprecated. Use 'checkServices[]' instead.",
+      );
       checkServices = ['mongo', 'search'];
       checkServices = ['mongo', 'search'];
     }
     }
     if (req.query.checkMiddlewaresStrictly != null) {
     if (req.query.checkMiddlewaresStrictly != null) {
-      logger.warn('The param \'checkMiddlewaresStrictly\' is deprecated. Use \'checkServices[]\' and \'strictly\' instead.');
+      logger.warn(
+        "The param 'checkMiddlewaresStrictly' is deprecated. Use 'checkServices[]' and 'strictly' instead.",
+      );
       checkServices = ['mongo', 'search'];
       checkServices = ['mongo', 'search'];
       isStrictly = true;
       isStrictly = true;
     }
     }

+ 199 - 141
apps/app/src/server/routes/apiv3/import.ts

@@ -1,5 +1,6 @@
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
+import type { Router } from 'express';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import type { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
 import type { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
@@ -8,11 +9,11 @@ import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { ImportSettings } from '~/server/service/import';
 import type { ImportSettings } from '~/server/service/import';
 import { getImportService } from '~/server/service/import';
 import { getImportService } from '~/server/service/import';
 import { generateOverwriteParams } from '~/server/service/import/overwrite-params';
 import { generateOverwriteParams } from '~/server/service/import/overwrite-params';
+import type { ZipFileStat } from '~/server/service/interfaces/export';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 
 
-
 const logger = loggerFactory('growi:routes:apiv3:import'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:import'); // eslint-disable-line no-unused-vars
 
 
 const path = require('path');
 const path = require('path');
@@ -20,7 +21,6 @@ const path = require('path');
 const express = require('express');
 const express = require('express');
 const multer = require('multer');
 const multer = require('multer');
 
 
-
 const router = express.Router();
 const router = express.Router();
 
 
 /**
 /**
@@ -126,7 +126,7 @@ const router = express.Router();
  *                  type: integer
  *                  type: integer
  *                  nullable: true
  *                  nullable: true
  */
  */
-export default function route(crowi: Crowi): void {
+export default function route(crowi: Crowi): Router {
   const { growiBridgeService, socketIoService } = crowi;
   const { growiBridgeService, socketIoService } = crowi;
   const importService = getImportService();
   const importService = getImportService();
 
 
@@ -201,22 +201,35 @@ export default function route(crowi: Crowi): void {
    *                        type: string
    *                        type: string
    *                        description: the access token of qiita.com
    *                        description: the access token of qiita.com
    */
    */
-  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => {
-    try {
-      const importSettingsParams = {
-        esaTeamName: await crowi.configManager.getConfig('importer:esa:team_name'),
-        esaAccessToken: await crowi.configManager.getConfig('importer:esa:access_token'),
-        qiitaTeamName: await crowi.configManager.getConfig('importer:qiita:team_name'),
-        qiitaAccessToken: await crowi.configManager.getConfig('importer:qiita:access_token'),
-      };
-      return res.apiv3({
-        importSettingsParams,
-      });
-    }
-    catch (err) {
-      return res.apiv3Err(err, 500);
-    }
-  });
+  router.get(
+    '/',
+    accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }),
+    loginRequired,
+    adminRequired,
+    async (req, res) => {
+      try {
+        const importSettingsParams = {
+          esaTeamName: await crowi.configManager.getConfig(
+            'importer:esa:team_name',
+          ),
+          esaAccessToken: await crowi.configManager.getConfig(
+            'importer:esa:access_token',
+          ),
+          qiitaTeamName: await crowi.configManager.getConfig(
+            'importer:qiita:team_name',
+          ),
+          qiitaAccessToken: await crowi.configManager.getConfig(
+            'importer:qiita:access_token',
+          ),
+        };
+        return res.apiv3({
+          importSettingsParams,
+        });
+      } catch (err) {
+        return res.apiv3Err(err, 500);
+      }
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -239,15 +252,20 @@ export default function route(crowi: Crowi): void {
    *                  status:
    *                  status:
    *                    $ref: '#/components/schemas/ImportStatus'
    *                    $ref: '#/components/schemas/ImportStatus'
    */
    */
-  router.get('/status', accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => {
-    try {
-      const status = await importService.getStatus();
-      return res.apiv3(status);
-    }
-    catch (err) {
-      return res.apiv3Err(err, 500);
-    }
-  });
+  router.get(
+    '/status',
+    accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }),
+    loginRequired,
+    adminRequired,
+    async (req, res) => {
+      try {
+        const status = await importService.getStatus();
+        return res.apiv3(status);
+      } catch (err) {
+        return res.apiv3Err(err, 500);
+      }
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -286,103 +304,131 @@ export default function route(crowi: Crowi): void {
    *        200:
    *        200:
    *          description: Import process has requested
    *          description: Import process has requested
    */
    */
-  router.post('/', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, addActivity, async(req, res) => {
-    // TODO: add express validator
-    const { fileName, collections, options } = req.body;
-
-    // pages collection can only be imported by upsert if isV5Compatible is true
-    const isV5Compatible = crowi.configManager.getConfig('app:isV5Compatible');
-    const isImportPagesCollection = collections.includes('pages');
-    if (isV5Compatible && isImportPagesCollection) {
-      /** @type {ImportOptionForPages} */
-      const option = options.find(opt => opt.collectionName === 'pages');
-      if (option.mode !== 'upsert') {
-        return res.apiv3Err(new ErrorV3('Upsert is only available for importing pages collection.', 'only_upsert_available'));
+  router.post(
+    '/',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }),
+    loginRequired,
+    adminRequired,
+    addActivity,
+    async (req, res) => {
+      // TODO: add express validator
+      const { fileName, collections, options } = req.body;
+
+      // pages collection can only be imported by upsert if isV5Compatible is true
+      const isV5Compatible =
+        crowi.configManager.getConfig('app:isV5Compatible');
+      const isImportPagesCollection = collections.includes('pages');
+      if (isV5Compatible && isImportPagesCollection) {
+        /** @type {ImportOptionForPages} */
+        const option = options.find((opt) => opt.collectionName === 'pages');
+        if (option.mode !== 'upsert') {
+          return res.apiv3Err(
+            new ErrorV3(
+              'Upsert is only available for importing pages collection.',
+              'only_upsert_available',
+            ),
+          );
+        }
       }
       }
-    }
-
-    const isMaintenanceMode = crowi.appService.isMaintenanceMode();
-    if (!isMaintenanceMode) {
-      return res.apiv3Err(new ErrorV3('GROWI is not maintenance mode. To import data, please activate the maintenance mode first.', 'not_maintenance_mode'));
-    }
 
 
+      const isMaintenanceMode = crowi.appService.isMaintenanceMode();
+      if (!isMaintenanceMode) {
+        return res.apiv3Err(
+          new ErrorV3(
+            'GROWI is not maintenance mode. To import data, please activate the maintenance mode first.',
+            'not_maintenance_mode',
+          ),
+        );
+      }
 
 
-    const zipFile = importService.getFile(fileName);
-
-    // return response first
-    res.apiv3();
+      const zipFile = importService.getFile(fileName);
 
 
-    /*
-     * unzip, parse
-     */
-    let meta;
-    let fileStatsToImport;
-    try {
-      // unzip
-      await importService.unzip(zipFile);
+      // return response first
+      res.apiv3();
 
 
-      // eslint-disable-next-line no-unused-vars
-      const parseZipResult = await growiBridgeService.parseZipFile(zipFile);
-      if (parseZipResult == null) {
-        throw new Error('parseZipFile returns null');
+      /*
+       * unzip, parse
+       */
+      let meta: object;
+      let fileStatsToImport: {
+        fileName: string;
+        collectionName: string;
+        size: number;
+      }[];
+      try {
+        // unzip
+        await importService.unzip(zipFile);
+
+        // eslint-disable-next-line no-unused-vars
+        const parseZipResult = await growiBridgeService.parseZipFile(zipFile);
+        if (parseZipResult == null) {
+          throw new Error('parseZipFile returns null');
+        }
+
+        meta = parseZipResult.meta;
+
+        // filter innerFileStats
+        fileStatsToImport = parseZipResult.innerFileStats.filter(
+          ({ collectionName }) => {
+            return collections.includes(collectionName);
+          },
+        );
+      } catch (err) {
+        logger.error(err);
+        adminEvent.emit('onErrorForImport', { message: err.message });
+        return;
       }
       }
 
 
-      meta = parseZipResult.meta;
+      /*
+       * validate with meta.json
+       */
+      try {
+        importService.validate(meta);
+      } catch (err) {
+        logger.error(err);
+        adminEvent.emit('onErrorForImport', { message: err.message });
+        return;
+      }
 
 
-      // filter innerFileStats
-      fileStatsToImport = parseZipResult.innerFileStats.filter(({ collectionName }) => {
-        return collections.includes(collectionName);
+      // generate maps of ImportSettings to import
+      // Use the Map for a potential fix for the code scanning alert no. 895: Prototype-polluting assignment
+      const importSettingsMap = new Map<string, ImportSettings>();
+      fileStatsToImport.forEach(({ fileName, collectionName }) => {
+        // instanciate GrowiArchiveImportOption
+        const option: GrowiArchiveImportOption = options.find(
+          (opt) => opt.collectionName === collectionName,
+        );
+
+        // generate options
+        const importSettings = {
+          mode: option.mode,
+          jsonFileName: fileName,
+          overwriteParams: generateOverwriteParams(
+            collectionName,
+            req.user._id,
+            option,
+          ),
+        } satisfies ImportSettings;
+
+        importSettingsMap.set(collectionName, importSettings);
       });
       });
-    }
-    catch (err) {
-      logger.error(err);
-      adminEvent.emit('onErrorForImport', { message: err.message });
-      return;
-    }
-
-    /*
-     * validate with meta.json
-     */
-    try {
-      importService.validate(meta);
-    }
-    catch (err) {
-      logger.error(err);
-      adminEvent.emit('onErrorForImport', { message: err.message });
-      return;
-    }
-
-    // generate maps of ImportSettings to import
-    // Use the Map for a potential fix for the code scanning alert no. 895: Prototype-polluting assignment
-    const importSettingsMap = new Map<string, ImportSettings>();
-    fileStatsToImport.forEach(({ fileName, collectionName }) => {
-      // instanciate GrowiArchiveImportOption
-      const option: GrowiArchiveImportOption = options.find(opt => opt.collectionName === collectionName);
-
-      // generate options
-      const importSettings = {
-        mode: option.mode,
-        jsonFileName: fileName,
-        overwriteParams: generateOverwriteParams(collectionName, req.user._id, option),
-      } satisfies ImportSettings;
-
-      importSettingsMap.set(collectionName, importSettings);
-    });
-
-    /*
-     * import
-     */
-    try {
-      importService.import(collections, importSettingsMap);
-
-      const parameters = { action: SupportedAction.ACTION_ADMIN_GROWI_DATA_IMPORTED };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-    }
-    catch (err) {
-      logger.error(err);
-      adminEvent.emit('onErrorForImport', { message: err.message });
-    }
-  });
+
+      /*
+       * import
+       */
+      try {
+        importService.import(collections, importSettingsMap);
+
+        const parameters = {
+          action: SupportedAction.ACTION_ADMIN_GROWI_DATA_IMPORTED,
+        };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+      } catch (err) {
+        logger.error(err);
+        adminEvent.emit('onErrorForImport', { message: err.message });
+      }
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -412,36 +458,43 @@ export default function route(crowi: Crowi): void {
    *              schema:
    *              schema:
    *                $ref: '#/components/schemas/FileImportResponse'
    *                $ref: '#/components/schemas/FileImportResponse'
    */
    */
-  router.post('/upload',
-    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, uploads.single('file'), addActivity,
-    async(req, res) => {
+  router.post(
+    '/upload',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }),
+    loginRequired,
+    adminRequired,
+    uploads.single('file'),
+    addActivity,
+    async (req, res) => {
       const { file } = req;
       const { file } = req;
       const zipFile = importService.getFile(file.filename);
       const zipFile = importService.getFile(file.filename);
-      let data;
+      let data: ZipFileStat | null;
 
 
       try {
       try {
         data = await growiBridgeService.parseZipFile(zipFile);
         data = await growiBridgeService.parseZipFile(zipFile);
-      }
-      catch (err) {
-      // TODO: use ApiV3Error
+      } catch (err) {
+        // TODO: use ApiV3Error
         logger.error(err);
         logger.error(err);
         return res.status(500).send({ status: 'ERROR' });
         return res.status(500).send({ status: 'ERROR' });
       }
       }
       try {
       try {
-      // validate with meta.json
-        importService.validate(data.meta);
+        // validate with meta.json
+        importService.validate(data?.meta);
 
 
-        const parameters = { action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_UPLOAD };
+        const parameters = {
+          action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
+        };
         activityEvent.emit('update', res.locals.activity._id, parameters);
         activityEvent.emit('update', res.locals.activity._id, parameters);
 
 
         return res.apiv3(data);
         return res.apiv3(data);
-      }
-      catch {
-        const msg = 'The version of this GROWI and the uploaded GROWI data are not the same';
+      } catch {
+        const msg =
+          'The version of this GROWI and the uploaded GROWI data are not the same';
         const validationErr = 'versions-are-not-met';
         const validationErr = 'versions-are-not-met';
         return res.apiv3Err(new ErrorV3(msg, validationErr), 500);
         return res.apiv3Err(new ErrorV3(msg, validationErr), 500);
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -458,17 +511,22 @@ export default function route(crowi: Crowi): void {
    *        200:
    *        200:
    *          description: all files are deleted
    *          description: all files are deleted
    */
    */
-  router.delete('/all', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => {
-    try {
-      importService.deleteAllZipFiles();
-
-      return res.apiv3();
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+  router.delete(
+    '/all',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }),
+    loginRequired,
+    adminRequired,
+    async (req, res) => {
+      try {
+        importService.deleteAllZipFiles();
+
+        return res.apiv3();
+      } catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err, 500);
+      }
+    },
+  );
 
 
   return router;
   return router;
 }
 }

+ 88 - 59
apps/app/src/server/routes/apiv3/in-app-notification.ts

@@ -1,17 +1,15 @@
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import express from 'express';
 import express from 'express';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 
 
 import type { IInAppNotification } from '../../../interfaces/in-app-notification';
 import type { IInAppNotification } from '../../../interfaces/in-app-notification';
-
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
-
 const router = express.Router();
 const router = express.Router();
 
 
 /**
 /**
@@ -88,7 +86,9 @@ const router = express.Router();
  */
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(
+    crowi,
+  );
   const addActivity = generateAddActivityMiddleware();
   const addActivity = generateAddActivityMiddleware();
 
 
   const inAppNotificationService = crowi.inAppNotificationService;
   const inAppNotificationService = crowi.inAppNotificationService;
@@ -97,7 +97,6 @@ module.exports = (crowi) => {
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
-
   /**
   /**
    * @swagger
    * @swagger
    *
    *
@@ -133,15 +132,21 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                $ref: '#/components/schemas/InAppNotificationListResponse'
    *                $ref: '#/components/schemas/InAppNotificationListResponse'
    */
    */
-  router.get('/list', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly,
-    async(req: CrowiRequest, res: ApiV3Response) => {
-    // user must be set by loginRequiredStrictly
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  router.get(
+    '/list',
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], {
+      acceptLegacy: true,
+    }),
+    loginRequiredStrictly,
+    async (req: CrowiRequest, res: ApiV3Response) => {
+      // user must be set by loginRequiredStrictly
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       const user = req.user!;
       const user = req.user!;
 
 
-      const limit = req.query.limit != null
-        ? parseInt(req.query.limit.toString()) || 10
-        : 10;
+      const limit =
+        req.query.limit != null
+          ? parseInt(req.query.limit.toString()) || 10
+          : 10;
 
 
       let offset = 0;
       let offset = 0;
       if (req.query.offset != null) {
       if (req.query.offset != null) {
@@ -158,37 +163,42 @@ module.exports = (crowi) => {
         Object.assign(queryOptions, { status: req.query.status });
         Object.assign(queryOptions, { status: req.query.status });
       }
       }
 
 
-      const paginationResult = await inAppNotificationService.getLatestNotificationsByUser(user._id, queryOptions);
-
-
-      const getActionUsersFromActivities = function(activities) {
-        return activities.map(({ user }) => user).filter((user, i, self) => self.indexOf(user) === i);
-      };
-
-      const serializedDocs: Array<IInAppNotification> = paginationResult.docs.map((doc) => {
-        if (doc.user != null && doc.user instanceof User) {
-          doc.user = serializeUserSecurely(doc.user);
-        }
-        // To add a new property into mongoose doc, need to change the format of doc to an object
-        const docObj: IInAppNotification = doc.toObject();
-        const actionUsersNew = getActionUsersFromActivities(doc.activities);
-
-        const serializedActionUsers = actionUsersNew.map((actionUser) => {
-          return serializeUserSecurely(actionUser);
+      const paginationResult =
+        await inAppNotificationService.getLatestNotificationsByUser(
+          user._id,
+          queryOptions,
+        );
+
+      const getActionUsersFromActivities = (activities) =>
+        activities
+          .map(({ user }) => user)
+          .filter((user, i, self) => self.indexOf(user) === i);
+
+      const serializedDocs: Array<IInAppNotification> =
+        paginationResult.docs.map((doc) => {
+          if (doc.user != null && doc.user instanceof User) {
+            doc.user = serializeUserSecurely(doc.user);
+          }
+          // To add a new property into mongoose doc, need to change the format of doc to an object
+          const docObj: IInAppNotification = doc.toObject();
+          const actionUsersNew = getActionUsersFromActivities(doc.activities);
+
+          const serializedActionUsers = actionUsersNew.map((actionUser) => {
+            return serializeUserSecurely(actionUser);
+          });
+
+          docObj.actionUsers = serializedActionUsers;
+          return docObj;
         });
         });
 
 
-        docObj.actionUsers = serializedActionUsers;
-        return docObj;
-      });
-
       const serializedPaginationResult = {
       const serializedPaginationResult = {
         ...paginationResult,
         ...paginationResult,
         docs: serializedDocs,
         docs: serializedDocs,
       };
       };
 
 
       return res.apiv3(serializedPaginationResult);
       return res.apiv3(serializedPaginationResult);
-    });
-
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -212,20 +222,27 @@ module.exports = (crowi) => {
    *                    type: integer
    *                    type: integer
    *                    description: Count of unread notifications
    *                    description: Count of unread notifications
    */
    */
-  router.get('/status', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly,
-    async(req: CrowiRequest, res: ApiV3Response) => {
-    // user must be set by loginRequiredStrictly
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  router.get(
+    '/status',
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], {
+      acceptLegacy: true,
+    }),
+    loginRequiredStrictly,
+    async (req: CrowiRequest, res: ApiV3Response) => {
+      // user must be set by loginRequiredStrictly
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       const user = req.user!;
       const user = req.user!;
 
 
       try {
       try {
-        const count = await inAppNotificationService.getUnreadCountByUser(user._id);
+        const count = await inAppNotificationService.getUnreadCountByUser(
+          user._id,
+        );
         return res.apiv3({ count });
         return res.apiv3({ count });
-      }
-      catch (err) {
+      } catch (err) {
         return res.apiv3Err(err);
         return res.apiv3Err(err);
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -256,10 +273,15 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                type: object
    *                type: object
    */
    */
-  router.post('/open', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly,
-    async(req: CrowiRequest, res: ApiV3Response) => {
-    // user must be set by loginRequiredStrictly
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  router.post(
+    '/open',
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], {
+      acceptLegacy: true,
+    }),
+    loginRequiredStrictly,
+    async (req: CrowiRequest, res: ApiV3Response) => {
+      // user must be set by loginRequiredStrictly
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       const user = req.user!;
       const user = req.user!;
 
 
       const id = req.body.id;
       const id = req.body.id;
@@ -268,11 +290,11 @@ module.exports = (crowi) => {
         const notification = await inAppNotificationService.open(user, id);
         const notification = await inAppNotificationService.open(user, id);
         const result = { notification };
         const result = { notification };
         return res.apiv3(result);
         return res.apiv3(result);
-      }
-      catch (err) {
+      } catch (err) {
         return res.apiv3Err(err);
         return res.apiv3Err(err);
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -289,24 +311,31 @@ module.exports = (crowi) => {
    *        200:
    *        200:
    *          description: All notifications opened successfully
    *          description: All notifications opened successfully
    */
    */
-  router.put('/all-statuses-open',
-    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly, addActivity,
-    async(req: CrowiRequest, res: ApiV3Response) => {
-    // user must be set by loginRequiredStrictly
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  router.put(
+    '/all-statuses-open',
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], {
+      acceptLegacy: true,
+    }),
+    loginRequiredStrictly,
+    addActivity,
+    async (req: CrowiRequest, res: ApiV3Response) => {
+      // user must be set by loginRequiredStrictly
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       const user = req.user!;
       const user = req.user!;
 
 
       try {
       try {
         await inAppNotificationService.updateAllNotificationsAsOpened(user);
         await inAppNotificationService.updateAllNotificationsAsOpened(user);
 
 
-        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN });
+        activityEvent.emit('update', res.locals.activity._id, {
+          action: SupportedAction.ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN,
+        });
 
 
         return res.apiv3();
         return res.apiv3();
-      }
-      catch (err) {
+      } catch (err) {
         return res.apiv3Err(err);
         return res.apiv3Err(err);
       }
       }
-    });
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 68 - 49
apps/app/src/server/routes/apiv3/installer.ts

@@ -1,3 +1,4 @@
+import type { IUser } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, Router } from 'express';
 import type { Request, Router } from 'express';
 import express from 'express';
 import express from 'express';
@@ -9,17 +10,20 @@ import loggerFactory from '~/utils/logger';
 import type Crowi from '../../crowi';
 import type Crowi from '../../crowi';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import * as applicationNotInstalled from '../../middlewares/application-not-installed';
 import * as applicationNotInstalled from '../../middlewares/application-not-installed';
-import { registerRules, registerValidation } from '../../middlewares/register-form-validator';
-import { InstallerService, FailedToCreateAdminUserError } from '../../service/installer';
-
+import {
+  registerRules,
+  registerValidation,
+} from '../../middlewares/register-form-validator';
+import {
+  FailedToCreateAdminUserError,
+  InstallerService,
+} from '../../service/installer';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
-
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:installer');
 const logger = loggerFactory('growi:routes:apiv3:installer');
 
 
-
-type FormRequest = Request & { form: any, logIn: any };
+type FormRequest = Request & { form: any; logIn: any };
 
 
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
   const addActivity = generateAddActivityMiddleware();
   const addActivity = generateAddActivityMiddleware();
@@ -78,53 +82,68 @@ module.exports = (crowi: Crowi): Router => {
    *                    example: Installation completed (Logged in as an admin user)
    *                    example: Installation completed (Logged in as an admin user)
    */
    */
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  router.post('/', registerRules(minPasswordLength), registerValidation, addActivity, async(req: FormRequest, res: ApiV3Response) => {
-
-    if (!req.form.isValid) {
-      const errors = req.form.errors;
-      return res.apiv3Err(errors, 400);
-    }
-
-    const registerForm = req.body.registerForm || {};
-
-    const name = registerForm.name;
-    const username = registerForm.username;
-    const email = registerForm.email;
-    const password = registerForm.password;
-    const language = registerForm['app:globalLang'] || 'en_US';
-
-    const installerService = new InstallerService(crowi);
-
-    let adminUser;
-    try {
-      adminUser = await installerService.install({
-        name,
-        username,
-        email,
-        password,
-      }, language);
-    }
-    catch (err) {
-      if (err instanceof FailedToCreateAdminUserError) {
-        return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_admin_user'));
+  router.post(
+    '/',
+    registerRules(minPasswordLength),
+    registerValidation,
+    addActivity,
+    async (req: FormRequest, res: ApiV3Response) => {
+      if (!req.form.isValid) {
+        const errors = req.form.errors;
+        return res.apiv3Err(errors, 400);
       }
       }
-      return res.apiv3Err(new ErrorV3(err, 'failed_to_install'));
-    }
 
 
-    await crowi.appService.setupAfterInstall();
-
-    const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
-    activityEvent.emit('update', res.locals.activity._id, parameters);
-
-    // login with passport
-    req.logIn(adminUser, (err) => {
-      if (err != null) {
-        return res.apiv3Err(new ErrorV3(err, 'failed_to_login_after_install'));
+      const registerForm = req.body.registerForm || {};
+
+      const name = registerForm.name;
+      const username = registerForm.username;
+      const email = registerForm.email;
+      const password = registerForm.password;
+      const language = registerForm['app:globalLang'] || 'en_US';
+
+      const installerService = new InstallerService(crowi);
+
+      let adminUser: IUser;
+      try {
+        adminUser = await installerService.install(
+          {
+            name,
+            username,
+            email,
+            password,
+          },
+          language,
+        );
+      } catch (err) {
+        if (err instanceof FailedToCreateAdminUserError) {
+          return res.apiv3Err(
+            new ErrorV3(err.message, 'failed_to_create_admin_user'),
+          );
+        }
+        return res.apiv3Err(new ErrorV3(err, 'failed_to_install'));
       }
       }
 
 
-      return res.apiv3({ message: 'Installation completed (Logged in as an admin user)' });
-    });
-  });
+      await crowi.appService.setupAfterInstall();
+
+      const parameters = {
+        action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS,
+      };
+      activityEvent.emit('update', res.locals.activity._id, parameters);
+
+      // login with passport
+      req.logIn(adminUser, (err) => {
+        if (err != null) {
+          return res.apiv3Err(
+            new ErrorV3(err, 'failed_to_login_after_install'),
+          );
+        }
+
+        return res.apiv3({
+          message: 'Installation completed (Logged in as an admin user)',
+        });
+      });
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 48 - 36
apps/app/src/server/routes/apiv3/invited.ts

@@ -6,16 +6,19 @@ import mongoose from 'mongoose';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import type Crowi from '../../crowi';
 import type Crowi from '../../crowi';
-import { invitedRules, invitedValidation } from '../../middlewares/invited-form-validator';
-
+import {
+  invitedRules,
+  invitedValidation,
+} from '../../middlewares/invited-form-validator';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
 const logger = loggerFactory('growi:routes:login');
 const logger = loggerFactory('growi:routes:login');
 
 
-type InvitedFormRequest = Request & { form: any, user: any };
+type InvitedFormRequest = Request & { form: any; user: any };
 
 
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
-  const applicationInstalled = require('../../middlewares/application-installed')(crowi);
+  const applicationInstalled =
+    require('../../middlewares/application-installed')(crowi);
   const router = express.Router();
   const router = express.Router();
 
 
   /**
   /**
@@ -59,44 +62,53 @@ module.exports = (crowi: Crowi): Router => {
    *                    type: string
    *                    type: string
    *                    description: URL to redirect after successful activation.
    *                    description: URL to redirect after successful activation.
    */
    */
-  router.post('/', applicationInstalled, invitedRules(), invitedValidation, async(req: InvitedFormRequest, res: ApiV3Response) => {
-    if (!req.user) {
-      return res.apiv3({ redirectTo: '/login' });
-    }
+  router.post(
+    '/',
+    applicationInstalled,
+    invitedRules(),
+    invitedValidation,
+    async (req: InvitedFormRequest, res: ApiV3Response) => {
+      if (!req.user) {
+        return res.apiv3({ redirectTo: '/login' });
+      }
 
 
-    if (!req.form.isValid) {
-      return res.apiv3Err(req.form.errors, 400);
-    }
+      if (!req.form.isValid) {
+        return res.apiv3Err(req.form.errors, 400);
+      }
 
 
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const User = mongoose.model<IUser, any>('User');
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const User = mongoose.model<IUser, any>('User');
 
 
-    const user = req.user;
-    const invitedForm = req.form.invitedForm || {};
-    const username = invitedForm.username;
-    const name = invitedForm.name;
-    const password = invitedForm.password;
+      const user = req.user;
+      const invitedForm = req.form.invitedForm || {};
+      const username = invitedForm.username;
+      const name = invitedForm.name;
+      const password = invitedForm.password;
 
 
-    // check user upper limit
-    const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
-    if (isUserCountExceedsUpperLimit) {
-      return res.apiv3Err('message.can_not_activate_maximum_number_of_users', 403);
-    }
+      // check user upper limit
+      const isUserCountExceedsUpperLimit =
+        await User.isUserCountExceedsUpperLimit();
+      if (isUserCountExceedsUpperLimit) {
+        return res.apiv3Err(
+          'message.can_not_activate_maximum_number_of_users',
+          403,
+        );
+      }
 
 
-    const creatable = await User.isRegisterableUsername(username);
-    if (!creatable) {
-      logger.debug('username', username);
-      return res.apiv3Err('message.unable_to_use_this_user', 403);
-    }
+      const creatable = await User.isRegisterableUsername(username);
+      if (!creatable) {
+        logger.debug('username', username);
+        return res.apiv3Err('message.unable_to_use_this_user', 403);
+      }
 
 
-    try {
-      await user.activateInvitedUser(username, name, password);
-      return res.apiv3({ redirectTo: '/' });
-    }
-    catch (err) {
-      return res.apiv3Err('message.failed_to_activate', 403);
-    }
-  });
+      try {
+        await user.activateInvitedUser(username, name, password);
+        return res.apiv3({ redirectTo: '/' });
+      } catch (err) {
+        return res.apiv3Err('message.failed_to_activate', 403);
+      }
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 116 - 63
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -1,12 +1,10 @@
-import type {
-  IPageInfoForListing, IPageInfo, IUserHasId,
-} from '@growi/core';
+import type { IPageInfo, IPageInfoForListing, IUserHasId } from '@growi/core';
 import { getIdForRef, isIPageInfoForEntity } from '@growi/core';
 import { getIdForRef, isIPageInfoForEntity } from '@growi/core';
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, Router } from 'express';
 import type { Request, Router } from 'express';
 import express from 'express';
 import express from 'express';
-import { query, oneOf } from 'express-validator';
+import { oneOf, query } from 'express-validator';
 import type { HydratedDocument } from 'mongoose';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
@@ -20,39 +18,37 @@ import loggerFactory from '~/utils/logger';
 import type Crowi from '../../crowi';
 import type Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import type { PageDocument, PageModel } from '../../models/page';
 import type { PageDocument, PageModel } from '../../models/page';
-
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
-
 const logger = loggerFactory('growi:routes:apiv3:page-tree');
 const logger = loggerFactory('growi:routes:apiv3:page-tree');
 
 
 /*
 /*
  * Types & Interfaces
  * Types & Interfaces
  */
  */
 interface AuthorizedRequest extends Request {
 interface AuthorizedRequest extends Request {
-  user?: IUserHasId,
+  user?: IUserHasId;
 }
 }
 
 
 /*
 /*
  * Validators
  * Validators
  */
  */
 const validator = {
 const validator = {
-  pagePathRequired: [
-    query('path').isString().withMessage('path is required'),
-  ],
-  pageIdOrPathRequired: oneOf([
-    query('id').isMongoId(),
-    query('path').isString(),
-  ], 'id or path is required'),
+  pagePathRequired: [query('path').isString().withMessage('path is required')],
+  pageIdOrPathRequired: oneOf(
+    [query('id').isMongoId(), query('path').isString()],
+    'id or path is required',
+  ),
   pageIdsOrPathRequired: [
   pageIdsOrPathRequired: [
     // type check independent of existence check
     // type check independent of existence check
-    query('pageIds').isArray().optional(),
+    query('pageIds')
+      .isArray()
+      .optional(),
     query('path').isString().optional(),
     query('path').isString().optional(),
     // existence check
     // existence check
-    oneOf([
-      query('pageIds').exists(),
-      query('path').exists(),
-    ], 'pageIds or path is required'),
+    oneOf(
+      [query('pageIds').exists(), query('path').exists()],
+      'pageIds or path is required',
+    ),
   ],
   ],
   infoParams: [
   infoParams: [
     query('attachBookmarkCount').isBoolean().optional(),
     query('attachBookmarkCount').isBoolean().optional(),
@@ -64,11 +60,13 @@ const validator = {
  * Routes
  * Routes
  */
  */
 const routerFactory = (crowi: Crowi): Router => {
 const routerFactory = (crowi: Crowi): Router => {
-  const loginRequired = require('../../middlewares/login-required')(crowi, true);
+  const loginRequired = require('../../middlewares/login-required')(
+    crowi,
+    true,
+  );
 
 
   const router = express.Router();
   const router = express.Router();
 
 
-
   /**
   /**
    * @swagger
    * @swagger
    *
    *
@@ -91,16 +89,20 @@ const routerFactory = (crowi: Crowi): Router => {
    *                 rootPage:
    *                 rootPage:
    *                   $ref: '#/components/schemas/PageForTreeItem'
    *                   $ref: '#/components/schemas/PageForTreeItem'
    */
    */
-  router.get('/root',
-    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.get(
+    '/root',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired,
+    async (req: AuthorizedRequest, res: ApiV3Response) => {
       try {
       try {
-        const rootPage: IPageForTreeItem = await pageListingService.findRootByViewer(req.user);
+        const rootPage: IPageForTreeItem =
+          await pageListingService.findRootByViewer(req.user);
         return res.apiv3({ rootPage });
         return res.apiv3({ rootPage });
-      }
-      catch (err) {
+      } catch (err) {
         return res.apiv3Err(new ErrorV3('rootPage not found'));
         return res.apiv3Err(new ErrorV3('rootPage not found'));
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -138,25 +140,39 @@ const routerFactory = (crowi: Crowi): Router => {
   /*
   /*
    * In most cases, using id should be prioritized
    * In most cases, using id should be prioritized
    */
    */
-  router.get('/children',
+  router.get(
+    '/children',
     accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
     accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
-    loginRequired, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    loginRequired,
+    validator.pageIdOrPathRequired,
+    apiV3FormValidator,
+    async (req: AuthorizedRequest, res: ApiV3Response) => {
       const { id, path } = req.query;
       const { id, path } = req.query;
 
 
-      const hideRestrictedByOwner = await configManager.getConfig('security:list-policy:hideRestrictedByOwner');
-      const hideRestrictedByGroup = await configManager.getConfig('security:list-policy:hideRestrictedByGroup');
+      const hideRestrictedByOwner = await configManager.getConfig(
+        'security:list-policy:hideRestrictedByOwner',
+      );
+      const hideRestrictedByGroup = await configManager.getConfig(
+        'security:list-policy:hideRestrictedByGroup',
+      );
 
 
       try {
       try {
-        const pages = await pageListingService.findChildrenByParentPathOrIdAndViewer(
-          (id || path) as string, req.user, !hideRestrictedByOwner, !hideRestrictedByGroup,
-        );
+        const pages =
+          await pageListingService.findChildrenByParentPathOrIdAndViewer(
+            (id || path) as string,
+            req.user,
+            !hideRestrictedByOwner,
+            !hideRestrictedByGroup,
+          );
         return res.apiv3({ children: pages });
         return res.apiv3({ children: pages });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Error occurred while finding children.', err);
         logger.error('Error occurred while finding children.', err);
-        return res.apiv3Err(new ErrorV3('Error occurred while finding children.'));
+        return res.apiv3Err(
+          new ErrorV3('Error occurred while finding children.'),
+        );
       }
       }
-    });
+    },
+  );
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -200,17 +216,26 @@ const routerFactory = (crowi: Crowi): Router => {
    *               additionalProperties:
    *               additionalProperties:
    *                 $ref: '#/components/schemas/PageInfoAll'
    *                 $ref: '#/components/schemas/PageInfoAll'
    */
    */
-  router.get('/info',
+  router.get(
+    '/info',
     accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
     accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
-    validator.pageIdsOrPathRequired, validator.infoParams, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    validator.pageIdsOrPathRequired,
+    validator.infoParams,
+    apiV3FormValidator,
+    async (req: AuthorizedRequest, res: ApiV3Response) => {
       const {
       const {
-        pageIds, path, attachBookmarkCount: attachBookmarkCountParam, attachShortBody: attachShortBodyParam,
+        pageIds,
+        path,
+        attachBookmarkCount: attachBookmarkCountParam,
+        attachShortBody: attachShortBodyParam,
       } = req.query;
       } = req.query;
 
 
       const attachBookmarkCount: boolean = attachBookmarkCountParam === 'true';
       const attachBookmarkCount: boolean = attachBookmarkCountParam === 'true';
       const attachShortBody: boolean = attachShortBodyParam === 'true';
       const attachShortBody: boolean = attachShortBodyParam === 'true';
 
 
-      const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
+      const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
+        'Page',
+      );
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       const Bookmark = mongoose.model<any, any>('Bookmark');
       const Bookmark = mongoose.model<any, any>('Bookmark');
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -219,32 +244,55 @@ const routerFactory = (crowi: Crowi): Router => {
       const pageGrantService: IPageGrantService = crowi.pageGrantService!;
       const pageGrantService: IPageGrantService = crowi.pageGrantService!;
 
 
       try {
       try {
-        const pages = pageIds != null
-          ? await Page.findByIdsAndViewer(pageIds as string[], req.user, null, true)
-          : await Page.findByPathAndViewer(path as string, req.user, null, false, true);
+        const pages =
+          pageIds != null
+            ? await Page.findByIdsAndViewer(
+                pageIds as string[],
+                req.user,
+                null,
+                true,
+              )
+            : await Page.findByPathAndViewer(
+                path as string,
+                req.user,
+                null,
+                false,
+                true,
+              );
 
 
-        const foundIds = pages.map(page => page._id);
+        const foundIds = pages.map((page) => page._id);
 
 
-        let shortBodiesMap;
+        let shortBodiesMap: Record<string, string | null> | undefined;
         if (attachShortBody) {
         if (attachShortBody) {
-          shortBodiesMap = await pageService.shortBodiesMapByPageIds(foundIds, req.user);
+          shortBodiesMap = await pageService.shortBodiesMapByPageIds(
+            foundIds,
+            req.user,
+          );
         }
         }
 
 
-        let bookmarkCountMap;
+        let bookmarkCountMap: Record<string, number> | undefined;
         if (attachBookmarkCount) {
         if (attachBookmarkCount) {
-          bookmarkCountMap = await Bookmark.getPageIdToCountMap(foundIds) as Record<string, number>;
+          bookmarkCountMap = (await Bookmark.getPageIdToCountMap(
+            foundIds,
+          )) as Record<string, number>;
         }
         }
 
 
-        const idToPageInfoMap: Record<string, IPageInfo | IPageInfoForListing> = {};
+        const idToPageInfoMap: Record<string, IPageInfo | IPageInfoForListing> =
+          {};
 
 
         const isGuestUser = req.user == null;
         const isGuestUser = req.user == null;
 
 
-        const userRelatedGroups = await pageGrantService.getUserRelatedGroups(req.user);
+        const userRelatedGroups = await pageGrantService.getUserRelatedGroups(
+          req.user,
+        );
 
 
         for (const page of pages) {
         for (const page of pages) {
           const basicPageInfo = {
           const basicPageInfo = {
             ...pageService.constructBasicPageInfo(page, isGuestUser),
             ...pageService.constructBasicPageInfo(page, isGuestUser),
-            bookmarkCount: bookmarkCountMap != null ? bookmarkCountMap[page._id.toString()] ?? 0 : 0,
+            bookmarkCount:
+              bookmarkCountMap != null
+                ? (bookmarkCountMap[page._id.toString()] ?? 0)
+                : 0,
           };
           };
 
 
           // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574)
           // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574)
@@ -256,24 +304,29 @@ const routerFactory = (crowi: Crowi): Router => {
             userRelatedGroups,
             userRelatedGroups,
           ); // use normal delete config
           ); // use normal delete config
 
 
-          const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
+          const pageInfo = !isIPageInfoForEntity(basicPageInfo)
             ? basicPageInfo
             ? basicPageInfo
-            : {
-              ...basicPageInfo,
-              isAbleToDeleteCompletely: canDeleteCompletely,
-              revisionShortBody: shortBodiesMap != null ? shortBodiesMap[page._id.toString()] : undefined,
-            } satisfies IPageInfoForListing;
+            : ({
+                ...basicPageInfo,
+                isAbleToDeleteCompletely: canDeleteCompletely,
+                revisionShortBody:
+                  shortBodiesMap != null
+                    ? (shortBodiesMap[page._id.toString()] ?? undefined)
+                    : undefined,
+              } satisfies IPageInfoForListing);
 
 
           idToPageInfoMap[page._id.toString()] = pageInfo;
           idToPageInfoMap[page._id.toString()] = pageInfo;
         }
         }
 
 
         return res.apiv3(idToPageInfoMap);
         return res.apiv3(idToPageInfoMap);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Error occurred while fetching page informations.', err);
         logger.error('Error occurred while fetching page informations.', err);
-        return res.apiv3Err(new ErrorV3('Error occurred while fetching page informations.'));
+        return res.apiv3Err(
+          new ErrorV3('Error occurred while fetching page informations.'),
+        );
       }
       }
-    });
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 145 - 84
apps/app/src/server/routes/apiv3/user-activation.ts

@@ -1,10 +1,9 @@
-import path from 'path';
-
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { format, subSeconds } from 'date-fns';
 import { format, subSeconds } from 'date-fns';
 import { body, validationResult } from 'express-validator';
 import { body, validationResult } from 'express-validator';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
+import path from 'path';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
@@ -34,7 +33,9 @@ export const completeRegistrationRules = () => {
       .matches(/^[\x20-\x7F]*$/)
       .matches(/^[\x20-\x7F]*$/)
       .withMessage('Password has invalid character')
       .withMessage('Password has invalid character')
       .isLength({ min: PASSOWRD_MINIMUM_NUMBER })
       .isLength({ min: PASSOWRD_MINIMUM_NUMBER })
-      .withMessage('Password minimum character should be more than 8 characters')
+      .withMessage(
+        'Password minimum character should be more than 8 characters',
+      )
       .not()
       .not()
       .isEmpty()
       .isEmpty()
       .withMessage('Password field is required'),
       .withMessage('Password field is required'),
@@ -49,12 +50,19 @@ export const validateCompleteRegistration = (req, res, next) => {
   }
   }
 
 
   const extractedErrors: string[] = [];
   const extractedErrors: string[] = [];
-  errors.array().map(err => extractedErrors.push(err.msg));
+  errors.array().map((err) => extractedErrors.push(err.msg));
 
 
   return res.apiv3Err(extractedErrors);
   return res.apiv3Err(extractedErrors);
 };
 };
 
 
-async function sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url) {
+async function sendEmailToAllAdmins(
+  userData,
+  admins,
+  appTitle,
+  mailService,
+  template,
+  url,
+) {
   admins.map((admin) => {
   admins.map((admin) => {
     return mailService.send({
     return mailService.send({
       to: admin.email,
       to: admin.email,
@@ -110,29 +118,47 @@ async function sendEmailToAllAdmins(userData, admins, appTitle, mailService, tem
  *                   type: string
  *                   type: string
  */
  */
 export const completeRegistrationAction = (crowi: Crowi) => {
 export const completeRegistrationAction = (crowi: Crowi) => {
-  const User = mongoose.model<IUser, { isEmailValid, isRegisterable, createUserByEmailAndPassword, findAdmins }>('User');
+  const User = mongoose.model<
+    IUser,
+    { isEmailValid; isRegisterable; createUserByEmailAndPassword; findAdmins }
+  >('User');
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
-  const {
-    aclService,
-    appService,
-    mailService,
-  } = crowi;
+  const { aclService, appService, mailService } = crowi;
 
 
-  return async function(req, res) {
+  return async (req, res) => {
     const { t } = await getTranslation();
     const { t } = await getTranslation();
 
 
     if (req.user != null) {
     if (req.user != null) {
-      return res.apiv3Err(new ErrorV3('You have been logged in', 'registration-failed'), 403);
+      return res.apiv3Err(
+        new ErrorV3('You have been logged in', 'registration-failed'),
+        403,
+      );
     }
     }
 
 
     // error when registration is not allowed
     // error when registration is not allowed
-    if (configManager.getConfig('security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED) {
-      return res.apiv3Err(new ErrorV3('Registration closed', 'registration-failed'), 403);
+    if (
+      configManager.getConfig('security:registrationMode') ===
+      aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED
+    ) {
+      return res.apiv3Err(
+        new ErrorV3('Registration closed', 'registration-failed'),
+        403,
+      );
     }
     }
 
 
     // error when email authentication is disabled
     // error when email authentication is disabled
-    if (configManager.getConfig('security:passport-local:isEmailAuthenticationEnabled') !== true) {
-      return res.apiv3Err(new ErrorV3('Email authentication configuration is disabled', 'registration-failed'), 403);
+    if (
+      configManager.getConfig(
+        'security:passport-local:isEmailAuthenticationEnabled',
+      ) !== true
+    ) {
+      return res.apiv3Err(
+        new ErrorV3(
+          'Email authentication configuration is disabled',
+          'registration-failed',
+        ),
+        403,
+      );
     }
     }
 
 
     const { userRegistrationOrder } = req;
     const { userRegistrationOrder } = req;
@@ -162,65 +188,94 @@ export const completeRegistrationAction = (crowi: Crowi) => {
         }
         }
       }
       }
       if (isError) {
       if (isError) {
-        return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403);
+        return res.apiv3Err(
+          new ErrorV3(errorMessage, 'registration-failed'),
+          403,
+        );
       }
       }
 
 
-      User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
-        if (err) {
-          if (err.name === 'UserUpperLimitException') {
-            errorMessage = t('message.can_not_register_maximum_number_of_users');
-          }
-          else {
-            errorMessage = t('message.failed_to_register');
-          }
-          return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403);
-        }
-
-        const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
-        activityEvent.emit('update', res.locals.activity._id, parameters);
-
-        userRegistrationOrder.revokeOneTimeToken();
-
-        if (configManager.getConfig('security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
-          const isMailerSetup = mailService.isMailerSetup ?? false;
-
-          if (isMailerSetup) {
-            const admins = await User.findAdmins();
-            const appTitle = appService.getAppTitle();
-            const locale = configManager.getConfig('app:globalLang');
-            const template = path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`);
-            const url = growiInfoService.getSiteUrl();
-
-            sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
-          }
-          // This 'completeRegistrationAction' should not be able to be called if the email settings is not set up in the first place.
-          // So this method dows not stop processing as an error, but only displays a warning. -- 2022.11.01 Yuki Takei
-          else {
-            logger.warn('E-mail Settings must be set up.');
-          }
-
-          return res.apiv3({});
-        }
-
-        req.login(userData, (err) => {
+      User.createUserByEmailAndPassword(
+        name,
+        username,
+        email,
+        password,
+        undefined,
+        async (err, userData) => {
           if (err) {
           if (err) {
-            logger.debug(err);
+            if (err.name === 'UserUpperLimitException') {
+              errorMessage = t(
+                'message.can_not_register_maximum_number_of_users',
+              );
+            } else {
+              errorMessage = t('message.failed_to_register');
+            }
+            return res.apiv3Err(
+              new ErrorV3(errorMessage, 'registration-failed'),
+              403,
+            );
           }
           }
-          else {
-            // update lastLoginAt
-            userData.updateLastLoginAt(new Date(), (err) => {
-              if (err) {
-                logger.error(`updateLastLoginAt dumps error: ${err}`);
-              }
-            });
+
+          const parameters = {
+            action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS,
+          };
+          activityEvent.emit('update', res.locals.activity._id, parameters);
+
+          userRegistrationOrder.revokeOneTimeToken();
+
+          if (
+            configManager.getConfig('security:registrationMode') ===
+            aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED
+          ) {
+            const isMailerSetup = mailService.isMailerSetup ?? false;
+
+            if (isMailerSetup) {
+              const admins = await User.findAdmins();
+              const appTitle = appService.getAppTitle();
+              const locale = configManager.getConfig('app:globalLang');
+              const template = path.join(
+                crowi.localeDir,
+                `${locale}/admin/userWaitingActivation.ejs`,
+              );
+              const url = growiInfoService.getSiteUrl();
+
+              sendEmailToAllAdmins(
+                userData,
+                admins,
+                appTitle,
+                mailService,
+                template,
+                url,
+              );
+            }
+            // This 'completeRegistrationAction' should not be able to be called if the email settings is not set up in the first place.
+            // So this method dows not stop processing as an error, but only displays a warning. -- 2022.11.01 Yuki Takei
+            else {
+              logger.warn('E-mail Settings must be set up.');
+            }
+
+            return res.apiv3({});
           }
           }
 
 
-          // userData.password can't be empty but, prepare redirect because password property in User Model is optional
-          // https://github.com/growilabs/growi/pull/6670
-          const redirectTo = userData.password != null ? '/' : '/me#password_settings';
-          return res.apiv3({ redirectTo });
-        });
-      });
+          req.login(userData, (err) => {
+            if (err) {
+              logger.debug(err);
+            } else {
+              // update lastLoginAt
+              userData.updateLastLoginAt(new Date(), (err) => {
+                if (err) {
+                  logger.error(`updateLastLoginAt dumps error: ${err}`);
+                }
+              });
+            }
+
+            // userData.password can't be empty but, prepare redirect because password property in User Model is optional
+            // https://github.com/growilabs/growi/pull/6670
+            const redirectTo =
+              userData.password != null ? '/' : '/me#password_settings';
+            return res.apiv3({ redirectTo });
+          });
+        },
+      );
     });
     });
   };
   };
 };
 };
@@ -244,17 +299,13 @@ export const validateRegisterForm = (req, res, next) => {
   }
   }
 
 
   const extractedErrors: string[] = [];
   const extractedErrors: string[] = [];
-  errors.array().map(err => extractedErrors.push(err.msg));
+  errors.array().map((err) => extractedErrors.push(err.msg));
 
 
   return res.apiv3Err(extractedErrors, 400);
   return res.apiv3Err(extractedErrors, 400);
 };
 };
 
 
 async function makeRegistrationEmailToken(email, crowi: Crowi) {
 async function makeRegistrationEmailToken(email, crowi: Crowi) {
-  const {
-    mailService,
-    localeDir,
-    appService,
-  } = crowi;
+  const { mailService, localeDir, appService } = crowi;
 
 
   const isMailerSetup = mailService.isMailerSetup ?? false;
   const isMailerSetup = mailService.isMailerSetup ?? false;
   if (!isMailerSetup) {
   if (!isMailerSetup) {
@@ -264,17 +315,24 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) {
   const locale = configManager.getConfig('app:globalLang');
   const locale = configManager.getConfig('app:globalLang');
   const appUrl = growiInfoService.getSiteUrl();
   const appUrl = growiInfoService.getSiteUrl();
 
 
-  const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
+  const userRegistrationOrder =
+    await UserRegistrationOrder.createUserRegistrationOrder(email);
   const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
   const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
   const expiredAt = subSeconds(userRegistrationOrder.expiredAt, grwTzoffsetSec);
   const expiredAt = subSeconds(userRegistrationOrder.expiredAt, grwTzoffsetSec);
   const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
   const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
-  const url = new URL(`/user-activation/${userRegistrationOrder.token}`, appUrl);
+  const url = new URL(
+    `/user-activation/${userRegistrationOrder.token}`,
+    appUrl,
+  );
   const oneTimeUrl = url.href;
   const oneTimeUrl = url.href;
 
 
   return mailService.send({
   return mailService.send({
     to: email,
     to: email,
     subject: '[GROWI] User Activation',
     subject: '[GROWI] User Activation',
-    template: path.join(localeDir, `${locale}/notifications/userActivation.ejs`),
+    template: path.join(
+      localeDir,
+      `${locale}/notifications/userActivation.ejs`,
+    ),
     vars: {
     vars: {
       appTitle: appService.getAppTitle(),
       appTitle: appService.getAppTitle(),
       email,
       email,
@@ -285,13 +343,17 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) {
 }
 }
 
 
 export const registerAction = (crowi: Crowi) => {
 export const registerAction = (crowi: Crowi) => {
-  const User = mongoose.model<IUser, { isRegisterableEmail, isEmailValid }>('User');
+  const User = mongoose.model<IUser, { isRegisterableEmail; isEmailValid }>(
+    'User',
+  );
 
 
-  return async function(req, res) {
+  return async (req, res) => {
     const registerForm = req.body.registerForm || {};
     const registerForm = req.body.registerForm || {};
     const email = registerForm.email;
     const email = registerForm.email;
     const isRegisterableEmail = await User.isRegisterableEmail(email);
     const isRegisterableEmail = await User.isRegisterableEmail(email);
-    const registrationMode = configManager.getConfig('security:registrationMode');
+    const registrationMode = configManager.getConfig(
+      'security:registrationMode',
+    );
     const isEmailValid = await User.isEmailValid(email);
     const isEmailValid = await User.isEmailValid(email);
 
 
     if (registrationMode === RegistrationMode.CLOSED) {
     if (registrationMode === RegistrationMode.CLOSED) {
@@ -309,8 +371,7 @@ export const registerAction = (crowi: Crowi) => {
 
 
     try {
     try {
       await makeRegistrationEmailToken(email, crowi);
       await makeRegistrationEmailToken(email, crowi);
-    }
-    catch (err) {
+    } catch (err) {
       return res.apiv3Err(err);
       return res.apiv3Err(err);
     }
     }
 
 

+ 46 - 26
apps/app/src/server/routes/apiv3/user-activities.ts

@@ -3,7 +3,7 @@ import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import type { Request, Router } from 'express';
 import type { Request, Router } from 'express';
 import express from 'express';
 import express from 'express';
 import { query } from 'express-validator';
 import { query } from 'express-validator';
-import type { PipelineStage, PaginateResult } from 'mongoose';
+import type { PaginateResult, PipelineStage } from 'mongoose';
 import { Types } from 'mongoose';
 import { Types } from 'mongoose';
 
 
 import type { IActivity } from '~/interfaces/activity';
 import type { IActivity } from '~/interfaces/activity';
@@ -12,22 +12,32 @@ import Activity from '~/server/models/activity';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 import type Crowi from '../../crowi';
 import type Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-
 import type { ApiV3Response } from './interfaces/apiv3-response';
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
 const logger = loggerFactory('growi:routes:apiv3:activity');
 const logger = loggerFactory('growi:routes:apiv3:activity');
 
 
 const validator = {
 const validator = {
   list: [
   list: [
-    query('limit').optional().isInt({ max: 100 }).withMessage('limit must be a number less than or equal to 100')
+    query('limit')
+      .optional()
+      .isInt({ max: 100 })
+      .withMessage('limit must be a number less than or equal to 100')
       .toInt(),
       .toInt(),
-    query('offset').optional().isInt().withMessage('page must be a number')
+    query('offset')
+      .optional()
+      .isInt()
+      .withMessage('page must be a number')
       .toInt(),
       .toInt(),
-    query('searchFilter').optional().isString().withMessage('query must be a string'),
-    query('targetUserId').optional().isMongoId().withMessage('user ID must be a MongoDB ID'),
+    query('searchFilter')
+      .optional()
+      .isString()
+      .withMessage('query must be a string'),
+    query('targetUserId')
+      .optional()
+      .isMongoId()
+      .withMessage('user ID must be a MongoDB ID'),
   ],
   ],
 };
 };
 
 
@@ -41,17 +51,16 @@ interface StrictActivityQuery {
 type CustomRequest<
 type CustomRequest<
   TQuery = Request['query'],
   TQuery = Request['query'],
   TBody = any,
   TBody = any,
-  TParams = any
+  TParams = any,
 > = Omit<Request<TParams, any, TBody, TQuery>, 'query'> & {
 > = Omit<Request<TParams, any, TBody, TQuery>, 'query'> & {
-    query: TQuery & Request['query'];
-    user?: IUserHasId;
+  query: TQuery & Request['query'];
+  user?: IUserHasId;
 };
 };
 
 
 type AuthorizedRequest = CustomRequest<StrictActivityQuery>;
 type AuthorizedRequest = CustomRequest<StrictActivityQuery>;
 
 
 type ActivityPaginationResult = PaginateResult<IActivity>;
 type ActivityPaginationResult = PaginateResult<IActivity>;
 
 
-
 /**
 /**
  * @swagger
  * @swagger
  *
  *
@@ -134,7 +143,9 @@ type ActivityPaginationResult = PaginateResult<IActivity>;
  */
  */
 
 
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(
+    crowi,
+  );
 
 
   const router = express.Router();
   const router = express.Router();
 
 
@@ -173,10 +184,15 @@ module.exports = (crowi: Crowi): Router => {
    *             schema:
    *             schema:
    *               $ref: '#/components/schemas/ActivityResponse'
    *               $ref: '#/components/schemas/ActivityResponse'
    */
    */
-  router.get('/',
-    loginRequiredStrictly, validator.list, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
-
-      const defaultLimit = configManager.getConfig('customize:showPageLimitationS');
+  router.get(
+    '/',
+    loginRequiredStrictly,
+    validator.list,
+    apiV3FormValidator,
+    async (req: AuthorizedRequest, res: ApiV3Response) => {
+      const defaultLimit = configManager.getConfig(
+        'customize:showPageLimitationS',
+      );
 
 
       const limit = req.query.limit || defaultLimit || 10;
       const limit = req.query.limit || defaultLimit || 10;
       const offset = req.query.offset || 0;
       const offset = req.query.offset || 0;
@@ -187,10 +203,12 @@ module.exports = (crowi: Crowi): Router => {
       }
       }
 
 
       if (!targetUserId) {
       if (!targetUserId) {
-        return res.apiv3Err('Target user ID is missing and authenticated user ID is unavailable.', 400);
+        return res.apiv3Err(
+          'Target user ID is missing and authenticated user ID is unavailable.',
+          400,
+        );
       }
       }
 
 
-
       try {
       try {
         const userObjectId = new Types.ObjectId(targetUserId);
         const userObjectId = new Types.ObjectId(targetUserId);
 
 
@@ -203,9 +221,7 @@ module.exports = (crowi: Crowi): Router => {
           },
           },
           {
           {
             $facet: {
             $facet: {
-              totalCount: [
-                { $count: 'count' },
-              ],
+              totalCount: [{ $count: 'count' }],
               docs: [
               docs: [
                 { $sort: { createdAt: -1 } },
                 { $sort: { createdAt: -1 } },
                 { $skip: offset },
                 { $skip: offset },
@@ -256,7 +272,8 @@ module.exports = (crowi: Crowi): Router => {
           },
           },
         ];
         ];
 
 
-        const [activityResults] = await Activity.aggregate(userActivityPipeline);
+        const [activityResults] =
+          await Activity.aggregate(userActivityPipeline);
 
 
         const serializedResults = activityResults.docs.map((doc: IActivity) => {
         const serializedResults = activityResults.docs.map((doc: IActivity) => {
           const { user, ...rest } = doc;
           const { user, ...rest } = doc;
@@ -266,7 +283,10 @@ module.exports = (crowi: Crowi): Router => {
           };
           };
         });
         });
 
 
-        const totalDocs = activityResults.totalCount.length > 0 ? activityResults.totalCount[0].count : 0;
+        const totalDocs =
+          activityResults.totalCount.length > 0
+            ? activityResults.totalCount[0].count
+            : 0;
         const totalPages = Math.ceil(totalDocs / limit);
         const totalPages = Math.ceil(totalDocs / limit);
         const page = Math.floor(offset / limit) + 1;
         const page = Math.floor(offset / limit) + 1;
 
 
@@ -289,12 +309,12 @@ module.exports = (crowi: Crowi): Router => {
         };
         };
 
 
         return res.apiv3({ serializedPaginationResult });
         return res.apiv3({ serializedPaginationResult });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Failed to get paginated activity', err);
         logger.error('Failed to get paginated activity', err);
         return res.apiv3Err(err, 500);
         return res.apiv3Err(err, 500);
       }
       }
-    });
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 51 - 45
apps/app/src/server/routes/apiv3/user-ui-settings.ts

@@ -13,10 +13,13 @@ const logger = loggerFactory('growi:routes:apiv3:user-ui-settings');
 const router = express.Router();
 const router = express.Router();
 
 
 module.exports = () => {
 module.exports = () => {
-
   const validatorForPut = [
   const validatorForPut = [
-    body('settings').exists().withMessage('The body param \'settings\' is required'),
-    body('settings.currentSidebarContents').optional().isIn(AllSidebarContentsType),
+    body('settings')
+      .exists()
+      .withMessage("The body param 'settings' is required"),
+    body('settings.currentSidebarContents')
+      .optional()
+      .isIn(AllSidebarContentsType),
     body('settings.currentProductNavWidth').optional().isNumeric(),
     body('settings.currentProductNavWidth').optional().isNumeric(),
     body('settings.preferCollapsedModeByUser').optional().isBoolean(),
     body('settings.preferCollapsedModeByUser').optional().isBoolean(),
   ];
   ];
@@ -67,55 +70,58 @@ module.exports = () => {
    *                   type: boolean
    *                   type: boolean
    */
    */
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  router.put('/', validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
-    const { user } = req;
-    const { settings } = req.body;
+  router.put(
+    '/',
+    validatorForPut,
+    apiV3FormValidator,
+    async (req: any, res: any) => {
+      const { user } = req;
+      const { settings } = req.body;
 
 
-    // extract only necessary params
-    const updateData = {
-      currentSidebarContents: settings.currentSidebarContents,
-      currentProductNavWidth: settings.currentProductNavWidth,
-      preferCollapsedModeByUser: settings.preferCollapsedModeByUser,
-    };
+      // extract only necessary params
+      const updateData = {
+        currentSidebarContents: settings.currentSidebarContents,
+        currentProductNavWidth: settings.currentProductNavWidth,
+        preferCollapsedModeByUser: settings.preferCollapsedModeByUser,
+      };
 
 
-    if (user == null) {
-      if (req.session.uiSettings == null) {
-        req.session.uiSettings = {};
+      if (user == null) {
+        if (req.session.uiSettings == null) {
+          req.session.uiSettings = {};
+        }
+        Object.keys(updateData).forEach((setting) => {
+          if (updateData[setting] != null) {
+            req.session.uiSettings[setting] = updateData[setting];
+          }
+        });
+        return res.apiv3(updateData);
       }
       }
-      Object.keys(updateData).forEach((setting) => {
-        if (updateData[setting] != null) {
-          req.session.uiSettings[setting] = updateData[setting];
+
+      // remove the keys that have null value
+      Object.keys(updateData).forEach((key) => {
+        if (updateData[key] == null) {
+          delete updateData[key];
         }
         }
       });
       });
-      return res.apiv3(updateData);
-    }
-
 
 
-    // remove the keys that have null value
-    Object.keys(updateData).forEach((key) => {
-      if (updateData[key] == null) {
-        delete updateData[key];
-      }
-    });
-
-    try {
-      const updatedSettings = await UserUISettings.findOneAndUpdate(
-        { user: user._id },
-        {
-          $set: {
-            user: user._id,
-            ...updateData,
+      try {
+        const updatedSettings = await UserUISettings.findOneAndUpdate(
+          { user: user._id },
+          {
+            $set: {
+              user: user._id,
+              ...updateData,
+            },
           },
           },
-        },
-        { upsert: true, new: true },
-      );
-      return res.apiv3(updatedSettings);
-    }
-    catch (err) {
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(err));
-    }
-  });
+          { upsert: true, new: true },
+        );
+        return res.apiv3(updatedSettings);
+      } catch (err) {
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(err));
+      }
+    },
+  );
 
 
   return router;
   return router;
 };
 };

+ 0 - 1
biome.json

@@ -31,7 +31,6 @@
       "!apps/app/src/client",
       "!apps/app/src/client",
       "!apps/app/src/server/middlewares",
       "!apps/app/src/server/middlewares",
       "!apps/app/src/server/routes/apiv3/*.js",
       "!apps/app/src/server/routes/apiv3/*.js",
-      "!apps/app/src/server/routes/apiv3/*.ts",
       "!apps/app/src/server/service"
       "!apps/app/src/server/service"
     ]
     ]
   },
   },