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

Merge pull request #10011 from weseek/imprv/166723-apply-modified-accesstokenparser-to-all-endpoints

imprv: Apply modified accessTokenParser to all endpoints
Shun Miyazawa 10 месяцев назад
Родитель
Сommit
23b712a71f
42 измененных файлов с 1067 добавлено и 1016 удалено
  1. 3 1
      apps/app/src/features/openai/server/routes/ai-assistant.ts
  2. 1 1
      apps/app/src/features/openai/server/routes/ai-assistants.ts
  3. 1 1
      apps/app/src/features/openai/server/routes/delete-ai-assistant.ts
  4. 1 1
      apps/app/src/features/openai/server/routes/delete-thread.ts
  5. 2 1
      apps/app/src/features/openai/server/routes/edit/index.ts
  6. 1 1
      apps/app/src/features/openai/server/routes/get-threads.ts
  7. 1 1
      apps/app/src/features/openai/server/routes/message/get-messages.ts
  8. 1 1
      apps/app/src/features/openai/server/routes/message/post-message.ts
  9. 2 1
      apps/app/src/features/openai/server/routes/set-default-ai-assistant.ts
  10. 1 1
      apps/app/src/features/openai/server/routes/thread.ts
  11. 1 1
      apps/app/src/features/openai/server/routes/update-ai-assistant.ts
  12. 1 0
      apps/app/src/features/page-bulk-export/server/routes/apiv3/page-bulk-export.ts
  13. 23 20
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  14. 80 79
      apps/app/src/server/routes/apiv3/activity.ts
  15. 139 134
      apps/app/src/server/routes/apiv3/app-settings.js
  16. 7 5
      apps/app/src/server/routes/apiv3/attachment.js
  17. 51 46
      apps/app/src/server/routes/apiv3/bookmark-folder.ts
  18. 69 65
      apps/app/src/server/routes/apiv3/bookmarks.js
  19. 3 3
      apps/app/src/server/routes/apiv3/export.js
  20. 56 56
      apps/app/src/server/routes/apiv3/g2g-transfer.ts
  21. 6 5
      apps/app/src/server/routes/apiv3/import.js
  22. 5 4
      apps/app/src/server/routes/apiv3/in-app-notification.ts
  23. 111 107
      apps/app/src/server/routes/apiv3/page-listing.ts
  24. 1 1
      apps/app/src/server/routes/apiv3/page/check-page-existence.ts
  25. 1 1
      apps/app/src/server/routes/apiv3/page/create-page.ts
  26. 1 1
      apps/app/src/server/routes/apiv3/page/get-page-paths-with-descendant-count.ts
  27. 1 1
      apps/app/src/server/routes/apiv3/page/get-yjs-data.ts
  28. 50 48
      apps/app/src/server/routes/apiv3/page/index.ts
  29. 1 1
      apps/app/src/server/routes/apiv3/page/publish-page.ts
  30. 1 1
      apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts
  31. 1 1
      apps/app/src/server/routes/apiv3/page/unpublish-page.ts
  32. 1 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  33. 95 95
      apps/app/src/server/routes/apiv3/pages/index.js
  34. 54 50
      apps/app/src/server/routes/apiv3/personal-setting/index.js
  35. 4 2
      apps/app/src/server/routes/apiv3/revisions.js
  36. 32 30
      apps/app/src/server/routes/apiv3/search.js
  37. 126 120
      apps/app/src/server/routes/apiv3/slack-integration-settings.js
  38. 1 1
      apps/app/src/server/routes/apiv3/user/get-related-groups.ts
  39. 114 112
      apps/app/src/server/routes/apiv3/users.js
  40. 14 14
      apps/app/src/server/routes/index.js
  41. 2 0
      packages/remark-attachment-refs/src/server/routes/refs.ts
  42. 1 1
      packages/remark-lsx/src/server/index.ts

+ 3 - 1
apps/app/src/features/openai/server/routes/ai-assistant.ts

@@ -29,7 +29,9 @@ export const createAiAssistantFactory: CreateAssistantFactory = (crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, upsertAiAssistantValidator, apiV3FormValidator,
+    accessTokenParser(
+      [SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true },
+    ), loginRequiredStrictly, certifyAiService, upsertAiAssistantValidator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const openaiService = getOpenaiService();
       if (openaiService == null) {

+ 1 - 1
apps/app/src/features/openai/server/routes/ai-assistants.ts

@@ -26,7 +26,7 @@ export const getAiAssistantsFactory: GetAiAssistantsFactory = (crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   return [
-    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService,
+    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService,
     async(req: Req, res: ApiV3Response) => {
       const openaiService = getOpenaiService();
       if (openaiService == null) {

+ 1 - 1
apps/app/src/features/openai/server/routes/delete-ai-assistant.ts

@@ -37,7 +37,7 @@ export const deleteAiAssistantsFactory: DeleteAiAssistantsFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { id } = req.params;
       const { user } = req;

+ 1 - 1
apps/app/src/features/openai/server/routes/delete-thread.ts

@@ -36,7 +36,7 @@ export const deleteThreadFactory: DeleteThreadFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { aiAssistantId, threadRelationId } = req.params;
       const { user } = req;

+ 2 - 1
apps/app/src/features/openai/server/routes/edit/index.ts

@@ -8,6 +8,7 @@ import { zodResponseFormat } from 'openai/helpers/zod';
 import type { MessageDelta } from 'openai/resources/beta/threads/messages.mjs';
 import { z } from 'zod';
 
+import { SCOPE } from '~/interfaces/scope';
 // Necessary imports
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
@@ -116,7 +117,7 @@ export const postMessageToEditHandlersFactory: PostMessageHandlersFactory = (cro
   ];
 
   return [
-    accessTokenParser, loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const {
         userMessage, markdown, threadId,

+ 1 - 1
apps/app/src/features/openai/server/routes/get-threads.ts

@@ -34,7 +34,7 @@ export const getThreadsFactory: GetThreadsFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const openaiService = getOpenaiService();
       if (openaiService == null) {

+ 1 - 1
apps/app/src/features/openai/server/routes/message/get-messages.ts

@@ -41,7 +41,7 @@ export const getMessagesFactory: GetMessagesFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const openaiService = getOpenaiService();
       if (openaiService == null) {

+ 1 - 1
apps/app/src/features/openai/server/routes/message/post-message.ts

@@ -54,7 +54,7 @@ export const postMessageHandlersFactory: PostMessageHandlersFactory = (crowi) =>
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { aiAssistantId, threadId } = req.body;
 

+ 2 - 1
apps/app/src/features/openai/server/routes/set-default-ai-assistant.ts

@@ -39,7 +39,8 @@ export const setDefaultAiAssistantFactory: setDefaultAiAssistantFactory = (crowi
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, adminRequired, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true }),
+    loginRequiredStrictly, adminRequired, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const openaiService = getOpenaiService();
       if (openaiService == null) {

+ 1 - 1
apps/app/src/features/openai/server/routes/thread.ts

@@ -38,7 +38,7 @@ export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: CreateThreadReq, res: ApiV3Response) => {
 
       const openaiService = getOpenaiService();

+ 1 - 1
apps/app/src/features/openai/server/routes/update-ai-assistant.ts

@@ -40,7 +40,7 @@ export const updateAiAssistantsFactory: UpdateAiAssistantsFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT], { acceptLegacy: true }), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { id } = req.params;
       const { user } = req;

+ 1 - 0
apps/app/src/features/page-bulk-export/server/routes/apiv3/page-bulk-export.ts

@@ -28,6 +28,7 @@ module.exports = (crowi: Crowi): Router => {
     ],
   };
 
+  // TODO: https://redmine.weseek.co.jp/issues/166911
   router.post('/', loginRequiredStrictly, validators.pageBulkExport, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const errors = validationResult(req);
     if (!errors.isEmpty()) {

+ 23 - 20
apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts

@@ -86,20 +86,22 @@ module.exports = (crowi: Crowi): Router => {
    *                   items:
    *                     type: object
    */
-  router.get('/orders', accessTokenParser([SCOPE.READ.FEATURES.QUESTIONNAIRE]), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const growiInfo = await growiInfoService.getGrowiInfo(true);
-    const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
+  router.get('/orders',
+    accessTokenParser([SCOPE.READ.FEATURES.QUESTIONNAIRE], { acceptLegacy: true }),
+    loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+      const growiInfo = await growiInfoService.getGrowiInfo(true);
+      const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
 
-    try {
-      const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
+      try {
+        const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
 
-      return res.apiv3({ questionnaireOrders });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+        return res.apiv3({ questionnaireOrders });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err, 500);
+      }
+    });
 
   /**
    * @swagger
@@ -123,10 +125,11 @@ module.exports = (crowi: Crowi): Router => {
    *                 isEnabled:
    *                   type: boolean
    */
-  router.get('/is-enabled', accessTokenParser([SCOPE.READ.FEATURES.QUESTIONNAIRE]), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const isEnabled = configManager.getConfig('questionnaire:isQuestionnaireEnabled');
-    return res.apiv3({ isEnabled });
-  });
+  router.get('/is-enabled',
+    accessTokenParser([SCOPE.READ.FEATURES.QUESTIONNAIRE], { acceptLegacy: true }), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+      const isEnabled = configManager.getConfig('questionnaire:isQuestionnaireEnabled');
+      return res.apiv3({ isEnabled });
+    });
 
 
   /**
@@ -154,7 +157,7 @@ module.exports = (crowi: Crowi): Router => {
    *             schema:
    *               type: object
    */
-  router.post('/proactive/answer', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
+  router.post('/proactive/answer', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE], { acceptLegacy: true }), loginRequired,
     validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const sendQuestionnaireAnswer = async() => {
         const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
@@ -238,7 +241,7 @@ module.exports = (crowi: Crowi): Router => {
    *       404:
    *         description: Not Found
    */
-  router.put('/answer', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
+  router.put('/answer', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE], { acceptLegacy: true }), loginRequired,
     validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
         const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
@@ -320,7 +323,7 @@ module.exports = (crowi: Crowi): Router => {
    *       404:
    *         description: Not Found
    */
-  router.put('/skip', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
+  router.put('/skip', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE], { acceptLegacy: true }), loginRequired,
     validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const errors = validationResult(req);
       if (!errors.isEmpty()) {
@@ -370,7 +373,7 @@ module.exports = (crowi: Crowi): Router => {
    *       404:
    *         description: Not Found
    */
-  router.put('/deny', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
+  router.put('/deny', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE], { acceptLegacy: true }), loginRequired,
     validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const errors = validationResult(req);
       if (!errors.isEmpty()) {

+ 80 - 79
apps/app/src/server/routes/apiv3/activity.ts

@@ -212,99 +212,100 @@ module.exports = (crowi: Crowi): Router => {
    *             schema:
    *               $ref: '#/components/schemas/ActivityResponse'
    */
-  // eslint-disable-next-line max-len
-  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.AUDIT_LOG]), loginRequiredStrictly, adminRequired, validator.list, apiV3FormValidator, async(req: Request, res: ApiV3Response) => {
-    const auditLogEnabled = configManager.getConfig('app:auditLogEnabled');
-    if (!auditLogEnabled) {
-      const msg = 'AuditLog is not enabled';
-      logger.error(msg);
-      return res.apiv3Err(msg, 405);
-    }
+  router.get('/',
+    accessTokenParser([SCOPE.READ.ADMIN.AUDIT_LOG], { acceptLegacy: true }),
+    loginRequiredStrictly, adminRequired, validator.list, apiV3FormValidator, async(req: Request, res: ApiV3Response) => {
+      const auditLogEnabled = configManager.getConfig('app:auditLogEnabled');
+      if (!auditLogEnabled) {
+        const msg = 'AuditLog is not enabled';
+        logger.error(msg);
+        return res.apiv3Err(msg, 405);
+      }
 
-    const limit = req.query.limit || configManager.getConfig('customize:showPageLimitationS');
-    const offset = req.query.offset || 1;
+      const limit = req.query.limit || configManager.getConfig('customize:showPageLimitationS');
+      const offset = req.query.offset || 1;
 
-    const query = {};
+      const query = {};
 
-    try {
-      const parsedSearchFilter = JSON.parse(req.query.searchFilter as string) as ISearchFilter;
+      try {
+        const parsedSearchFilter = JSON.parse(req.query.searchFilter as string) as ISearchFilter;
 
-      // add username to query
-      const canContainUsernameFilterToQuery = (
-        parsedSearchFilter.usernames != null
+        // add username to query
+        const canContainUsernameFilterToQuery = (
+          parsedSearchFilter.usernames != null
         && parsedSearchFilter.usernames.length > 0
         && parsedSearchFilter.usernames.every(u => typeof u === 'string')
-      );
-      if (canContainUsernameFilterToQuery) {
-        Object.assign(query, { 'snapshot.username': parsedSearchFilter.usernames });
-      }
+        );
+        if (canContainUsernameFilterToQuery) {
+          Object.assign(query, { 'snapshot.username': parsedSearchFilter.usernames });
+        }
 
-      // add action to query
-      if (parsedSearchFilter.actions != null) {
-        const availableActions = crowi.activityService.getAvailableActions(false);
-        const searchableActions = parsedSearchFilter.actions.filter(action => availableActions.includes(action));
-        Object.assign(query, { action: searchableActions });
-      }
+        // add action to query
+        if (parsedSearchFilter.actions != null) {
+          const availableActions = crowi.activityService.getAvailableActions(false);
+          const searchableActions = parsedSearchFilter.actions.filter(action => availableActions.includes(action));
+          Object.assign(query, { action: searchableActions });
+        }
 
-      // add date to query
-      const startDate = parseISO(parsedSearchFilter?.dates?.startDate || '');
-      const endDate = parseISO(parsedSearchFilter?.dates?.endDate || '');
-      if (isValid(startDate) && isValid(endDate)) {
-        Object.assign(query, {
-          createdAt: {
-            $gte: startDate,
-            // + 23 hours 59 minutes
-            $lt: addMinutes(endDate, 1439),
-          },
-        });
+        // add date to query
+        const startDate = parseISO(parsedSearchFilter?.dates?.startDate || '');
+        const endDate = parseISO(parsedSearchFilter?.dates?.endDate || '');
+        if (isValid(startDate) && isValid(endDate)) {
+          Object.assign(query, {
+            createdAt: {
+              $gte: startDate,
+              // + 23 hours 59 minutes
+              $lt: addMinutes(endDate, 1439),
+            },
+          });
+        }
+        else if (isValid(startDate) && !isValid(endDate)) {
+          Object.assign(query, {
+            createdAt: {
+              $gte: startDate,
+              // + 23 hours 59 minutes
+              $lt: addMinutes(startDate, 1439),
+            },
+          });
+        }
       }
-      else if (isValid(startDate) && !isValid(endDate)) {
-        Object.assign(query, {
-          createdAt: {
-            $gte: startDate,
-            // + 23 hours 59 minutes
-            $lt: addMinutes(startDate, 1439),
-          },
-        });
+      catch (err) {
+        logger.error('Invalid value', err);
+        return res.apiv3Err(err, 400);
       }
-    }
-    catch (err) {
-      logger.error('Invalid value', err);
-      return res.apiv3Err(err, 400);
-    }
 
-    try {
-      const paginateResult = await Activity.paginate(
-        query,
-        {
-          lean: true,
-          limit,
-          offset,
-          sort: { createdAt: -1 },
-          populate: 'user',
-        },
-      );
+      try {
+        const paginateResult = await Activity.paginate(
+          query,
+          {
+            lean: true,
+            limit,
+            offset,
+            sort: { createdAt: -1 },
+            populate: 'user',
+          },
+        );
 
-      const serializedDocs = paginateResult.docs.map((doc: IActivity) => {
-        const { user, ...rest } = doc;
-        return {
-          user: serializeUserSecurely(user),
-          ...rest,
-        };
-      });
+        const serializedDocs = paginateResult.docs.map((doc: IActivity) => {
+          const { user, ...rest } = doc;
+          return {
+            user: serializeUserSecurely(user),
+            ...rest,
+          };
+        });
 
-      const serializedPaginationResult = {
-        ...paginateResult,
-        docs: serializedDocs,
-      };
+        const serializedPaginationResult = {
+          ...paginateResult,
+          docs: serializedDocs,
+        };
 
-      return res.apiv3({ serializedPaginationResult });
-    }
-    catch (err) {
-      logger.error('Failed to get paginated activity', err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+        return res.apiv3({ serializedPaginationResult });
+      }
+      catch (err) {
+        logger.error('Failed to get paginated activity', err);
+        return res.apiv3Err(err, 500);
+      }
+    });
 
   return router;
 };

+ 139 - 134
apps/app/src/server/routes/apiv3/app-settings.js

@@ -436,7 +436,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/AppSettingParams'
    */
-  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.APP]), loginRequiredStrictly, adminRequired, async(req, res) => {
+  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.APP], { acceptLegacy: true }), loginRequiredStrictly, adminRequired, async(req, res) => {
     const appSettingsParams = {
       title: configManager.getConfig('app:title'),
       confidential: configManager.getConfig('app:confidential'),
@@ -899,87 +899,88 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/FileUploadSettingParams'
    */
-  //  eslint-disable-next-line max-len
-  router.put('/file-upload-setting', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity, validator.fileUploadSetting, apiV3FormValidator, async(req, res) => {
-    const { fileUploadType } = req.body;
+  router.put('/file-upload-setting',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired,
+    addActivity, validator.fileUploadSetting, apiV3FormValidator, async(req, res) => {
+      const { fileUploadType } = req.body;
 
-    const requestParams = {
-      'app:fileUploadType': fileUploadType,
-    };
+      const requestParams = {
+        'app:fileUploadType': fileUploadType,
+      };
 
-    if (fileUploadType === 'gcs') {
-      requestParams['gcs:apiKeyJsonPath'] = req.body.gcsApiKeyJsonPath;
-      requestParams['gcs:bucket'] = req.body.gcsBucket;
-      requestParams['gcs:uploadNamespace'] = req.body.gcsUploadNamespace;
-      requestParams['gcs:referenceFileWithRelayMode'] = req.body.gcsReferenceFileWithRelayMode;
-    }
+      if (fileUploadType === 'gcs') {
+        requestParams['gcs:apiKeyJsonPath'] = req.body.gcsApiKeyJsonPath;
+        requestParams['gcs:bucket'] = req.body.gcsBucket;
+        requestParams['gcs:uploadNamespace'] = req.body.gcsUploadNamespace;
+        requestParams['gcs:referenceFileWithRelayMode'] = req.body.gcsReferenceFileWithRelayMode;
+      }
 
-    if (fileUploadType === 'aws') {
-      requestParams['aws:s3Region'] = req.body.s3Region;
-      requestParams['aws:s3CustomEndpoint'] = req.body.s3CustomEndpoint;
-      requestParams['aws:s3Bucket'] = req.body.s3Bucket;
-      requestParams['aws:s3AccessKeyId'] = req.body.s3AccessKeyId;
-      requestParams['aws:referenceFileWithRelayMode'] = req.body.s3ReferenceFileWithRelayMode;
-    }
+      if (fileUploadType === 'aws') {
+        requestParams['aws:s3Region'] = req.body.s3Region;
+        requestParams['aws:s3CustomEndpoint'] = req.body.s3CustomEndpoint;
+        requestParams['aws:s3Bucket'] = req.body.s3Bucket;
+        requestParams['aws:s3AccessKeyId'] = req.body.s3AccessKeyId;
+        requestParams['aws:referenceFileWithRelayMode'] = req.body.s3ReferenceFileWithRelayMode;
+      }
 
-    if (fileUploadType === 'azure') {
-      requestParams['azure:tenantId'] = req.body.azureTenantId;
-      requestParams['azure:clientId'] = req.body.azureClientId;
-      requestParams['azure:clientSecret'] = req.body.azureClientSecret;
-      requestParams['azure:storageAccountName'] = req.body.azureStorageAccountName;
-      requestParams['azure:storageContainerName'] = req.body.azureStorageContainerName;
-      requestParams['azure:referenceFileWithRelayMode'] = req.body.azureReferenceFileWithRelayMode;
-    }
+      if (fileUploadType === 'azure') {
+        requestParams['azure:tenantId'] = req.body.azureTenantId;
+        requestParams['azure:clientId'] = req.body.azureClientId;
+        requestParams['azure:clientSecret'] = req.body.azureClientSecret;
+        requestParams['azure:storageAccountName'] = req.body.azureStorageAccountName;
+        requestParams['azure:storageContainerName'] = req.body.azureStorageContainerName;
+        requestParams['azure:referenceFileWithRelayMode'] = req.body.azureReferenceFileWithRelayMode;
+      }
 
-    try {
-      await configManager.updateConfigs(requestParams, { skipPubsub: true });
+      try {
+        await configManager.updateConfigs(requestParams, { skipPubsub: true });
 
-      const s3SecretAccessKey = req.body.s3SecretAccessKey;
-      if (fileUploadType === 'aws' && s3SecretAccessKey != null && s3SecretAccessKey.trim() !== '') {
-        await configManager.updateConfigs({ 'aws:s3SecretAccessKey': s3SecretAccessKey }, { skipPubsub: true });
-      }
+        const s3SecretAccessKey = req.body.s3SecretAccessKey;
+        if (fileUploadType === 'aws' && s3SecretAccessKey != null && s3SecretAccessKey.trim() !== '') {
+          await configManager.updateConfigs({ 'aws:s3SecretAccessKey': s3SecretAccessKey }, { skipPubsub: true });
+        }
 
-      await crowi.setUpFileUpload(true);
-      crowi.fileUploaderSwitchService.publishUpdatedMessage();
+        await crowi.setUpFileUpload(true);
+        crowi.fileUploaderSwitchService.publishUpdatedMessage();
 
-      const responseParams = {
-        fileUploadType: configManager.getConfig('app:fileUploadType'),
-      };
+        const responseParams = {
+          fileUploadType: configManager.getConfig('app:fileUploadType'),
+        };
 
-      if (fileUploadType === 'gcs') {
-        responseParams.gcsApiKeyJsonPath = configManager.getConfig('gcs:apiKeyJsonPath');
-        responseParams.gcsBucket = configManager.getConfig('gcs:bucket');
-        responseParams.gcsUploadNamespace = configManager.getConfig('gcs:uploadNamespace');
-        responseParams.gcsReferenceFileWithRelayMode = configManager.getConfig('gcs:referenceFileWithRelayMode ');
-      }
+        if (fileUploadType === 'gcs') {
+          responseParams.gcsApiKeyJsonPath = configManager.getConfig('gcs:apiKeyJsonPath');
+          responseParams.gcsBucket = configManager.getConfig('gcs:bucket');
+          responseParams.gcsUploadNamespace = configManager.getConfig('gcs:uploadNamespace');
+          responseParams.gcsReferenceFileWithRelayMode = configManager.getConfig('gcs:referenceFileWithRelayMode ');
+        }
 
-      if (fileUploadType === 'aws') {
-        responseParams.s3Region = configManager.getConfig('aws:s3Region');
-        responseParams.s3CustomEndpoint = configManager.getConfig('aws:s3CustomEndpoint');
-        responseParams.s3Bucket = configManager.getConfig('aws:s3Bucket');
-        responseParams.s3AccessKeyId = configManager.getConfig('aws:s3AccessKeyId');
-        responseParams.s3ReferenceFileWithRelayMode = configManager.getConfig('aws:referenceFileWithRelayMode');
-      }
+        if (fileUploadType === 'aws') {
+          responseParams.s3Region = configManager.getConfig('aws:s3Region');
+          responseParams.s3CustomEndpoint = configManager.getConfig('aws:s3CustomEndpoint');
+          responseParams.s3Bucket = configManager.getConfig('aws:s3Bucket');
+          responseParams.s3AccessKeyId = configManager.getConfig('aws:s3AccessKeyId');
+          responseParams.s3ReferenceFileWithRelayMode = configManager.getConfig('aws:referenceFileWithRelayMode');
+        }
 
-      if (fileUploadType === 'azure') {
-        responseParams.azureTenantId = configManager.getConfig('azure:tenantId');
-        responseParams.azureClientId = configManager.getConfig('azure:clientId');
-        responseParams.azureClientSecret = configManager.getConfig('azure:clientSecret');
-        responseParams.azureStorageAccountName = configManager.getConfig('azure:storageAccountName');
-        responseParams.azureStorageContainerName = configManager.getConfig('azure:storageContainerName');
-        responseParams.azureReferenceFileWithRelayMode = configManager.getConfig('azure:referenceFileWithRelayMode');
+        if (fileUploadType === 'azure') {
+          responseParams.azureTenantId = configManager.getConfig('azure:tenantId');
+          responseParams.azureClientId = configManager.getConfig('azure:clientId');
+          responseParams.azureClientSecret = configManager.getConfig('azure:clientSecret');
+          responseParams.azureStorageAccountName = configManager.getConfig('azure:storageAccountName');
+          responseParams.azureStorageContainerName = configManager.getConfig('azure:storageContainerName');
+          responseParams.azureReferenceFileWithRelayMode = configManager.getConfig('azure:referenceFileWithRelayMode');
+        }
+        const parameters = { action: SupportedAction.ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+        return res.apiv3({ responseParams });
+      }
+      catch (err) {
+        const msg = 'Error occurred in updating fileUploadType';
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
       }
-      const parameters = { action: SupportedAction.ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-      return res.apiv3({ responseParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating fileUploadType';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
-    }
 
-  });
+    });
 
   /**
    * @swagger
@@ -1010,36 +1011,38 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/QuestionnaireSettingParams'
    */
-  // eslint-disable-next-line max-len
-  router.put('/questionnaire-settings', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
-    const { isQuestionnaireEnabled, isAppSiteUrlHashed } = req.body;
+  router.put('/questionnaire-settings',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity,
+    validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
+      const { isQuestionnaireEnabled, isAppSiteUrlHashed } = req.body;
 
-    const requestParams = {
-      'questionnaire:isQuestionnaireEnabled': isQuestionnaireEnabled,
-      'questionnaire:isAppSiteUrlHashed': isAppSiteUrlHashed,
-    };
+      const requestParams = {
+        'questionnaire:isQuestionnaireEnabled': isQuestionnaireEnabled,
+        'questionnaire:isAppSiteUrlHashed': isAppSiteUrlHashed,
+      };
 
-    try {
-      await configManager.updateConfigs(requestParams, { skipPubsub: true });
+      try {
+        await configManager.updateConfigs(requestParams, { skipPubsub: true });
 
-      const responseParams = {
-        isQuestionnaireEnabled: configManager.getConfig('questionnaire:isQuestionnaireEnabled'),
-        isAppSiteUrlHashed: configManager.getConfig('questionnaire:isAppSiteUrlHashed'),
-      };
+        const responseParams = {
+          isQuestionnaireEnabled: configManager.getConfig('questionnaire:isQuestionnaireEnabled'),
+          isAppSiteUrlHashed: configManager.getConfig('questionnaire:isAppSiteUrlHashed'),
+        };
 
-      const parameters = { action: SupportedAction.ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-      return res.apiv3({ responseParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating questionnaire settings';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-questionnaire-settings-failed'));
-    }
+        const parameters = { action: SupportedAction.ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+        return res.apiv3({ responseParams });
+      }
+      catch (err) {
+        const msg = 'Error occurred in updating questionnaire settings';
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'update-questionnaire-settings-failed'));
+      }
 
-  });
+    });
 
-  router.put('/page-bulk-export-settings', loginRequiredStrictly, adminRequired, addActivity, validator.pageBulkExportSettings, apiV3FormValidator,
+  router.put('/page-bulk-export-settings',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity, validator.pageBulkExportSettings, apiV3FormValidator,
     async(req, res) => {
       const requestParams = {
         'app:isBulkExportPagesEnabled': req.body.isBulkExportPagesEnabled,
@@ -1091,26 +1094,27 @@ module.exports = (crowi) => {
    *                      description: is V5 compatible, or not
    *                      example: true
    */
-  router.post('/v5-schema-migration', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, async(req, res) => {
-    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'));
-    }
+  router.post('/v5-schema-migration',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP], { acceptLegacy: true }), loginRequiredStrictly, adminRequired, async(req, res) => {
+      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 isV5Compatible = configManager.getConfig('app:isV5Compatible');
+      const isV5Compatible = configManager.getConfig('app:isV5Compatible');
 
-    try {
-      if (!isV5Compatible) {
+      try {
+        if (!isV5Compatible) {
         // This method throws and emit socketIo event when error occurs
-        crowi.pageService.normalizeAllPublicPages();
+          crowi.pageService.normalizeAllPublicPages();
+        }
+      }
+      catch (err) {
+        return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
       }
-    }
-    catch (err) {
-      return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
-    }
 
-    return res.apiv3({ isV5Compatible });
-  });
+      return res.apiv3({ isV5Compatible });
+    });
 
   /**
    * @swagger
@@ -1146,36 +1150,37 @@ module.exports = (crowi) => {
    *                      description: true if maintenance mode is enabled
    *                      example: true
    */
-  // eslint-disable-next-line max-len
-  router.post('/maintenance-mode', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity, validator.maintenanceMode, apiV3FormValidator, async(req, res) => {
-    const { flag } = req.body;
-    const parameters = {};
-    try {
-      if (flag) {
-        await crowi.appService.startMaintenanceMode();
-        Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_ENABLED });
-      }
-      else {
-        await crowi.appService.endMaintenanceMode();
-        Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_DISABLED });
-      }
-    }
-    catch (err) {
-      logger.error(err);
-      if (flag) {
-        res.apiv3Err(new ErrorV3('Failed to start maintenance mode', 'failed_to_start_maintenance_mode'), 500);
+  router.post('/maintenance-mode',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP], { acceptLegacy: true }),
+    loginRequiredStrictly, adminRequired, addActivity, validator.maintenanceMode, apiV3FormValidator, async(req, res) => {
+      const { flag } = req.body;
+      const parameters = {};
+      try {
+        if (flag) {
+          await crowi.appService.startMaintenanceMode();
+          Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_ENABLED });
+        }
+        else {
+          await crowi.appService.endMaintenanceMode();
+          Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_DISABLED });
+        }
       }
-      else {
-        res.apiv3Err(new ErrorV3('Failed to end maintenance mode', 'failed_to_end_maintenance_mode'), 500);
+      catch (err) {
+        logger.error(err);
+        if (flag) {
+          res.apiv3Err(new ErrorV3('Failed to start maintenance mode', 'failed_to_start_maintenance_mode'), 500);
+        }
+        else {
+          res.apiv3Err(new ErrorV3('Failed to end maintenance mode', 'failed_to_end_maintenance_mode'), 500);
+        }
       }
-    }
 
-    if ('action' in parameters) {
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-    }
+      if ('action' in parameters) {
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+      }
 
-    res.apiv3({ flag });
-  });
+      res.apiv3({ flag });
+    });
 
   return router;
 };

+ 7 - 5
apps/app/src/server/routes/apiv3/attachment.js

@@ -199,7 +199,8 @@ module.exports = (crowi) => {
    *                  type: object
    *                  $ref: '#/components/schemas/AttachmentPaginateResult'
    */
-  router.get('/list', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), loginRequired, validator.retrieveAttachments, apiV3FormValidator,
+  router.get('/list',
+    accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT], { acceptLegacy: true }), loginRequired, validator.retrieveAttachments, apiV3FormValidator,
     async(req, res) => {
 
       const limit = req.query.limit || await crowi.configManager.getConfig('customize:showPageLimitationS') || 10;
@@ -274,7 +275,8 @@ module.exports = (crowi) => {
    *          500:
    *            $ref: '#/components/responses/500'
    */
-  router.get('/limit', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator,
+  router.get('/limit',
+    accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT], { acceptLegacy: true }), loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator,
     async(req, res) => {
       const { fileUploadService } = crowi;
       const fileSize = Number(req.query.fileSize);
@@ -342,8 +344,8 @@ module.exports = (crowi) => {
    *          500:
    *            $ref: '#/components/responses/500'
    */
-  router.post('/', uploads.single('file'), accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT]), loginRequiredStrictly, excludeReadOnlyUser,
-    validator.retrieveAddAttachment, apiV3FormValidator, addActivity,
+  router.post('/', uploads.single('file'), accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], { acceptLegacy: true }),
+    loginRequiredStrictly, excludeReadOnlyUser, validator.retrieveAddAttachment, apiV3FormValidator, addActivity,
     // Removed autoReap middleware to use file data in asynchronous processes. Instead, implemented file deletion after asynchronous processes complete
     async(req, res) => {
 
@@ -407,7 +409,7 @@ module.exports = (crowi) => {
    *            schema:
    *              type: string
    */
-  router.get('/:id', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), certifySharedPageAttachmentMiddleware, loginRequired,
+  router.get('/:id', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT], { acceptLegacy: true }), certifySharedPageAttachmentMiddleware, loginRequired,
     validator.retrieveAttachment, apiV3FormValidator,
     async(req, res) => {
       try {

+ 51 - 46
apps/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -158,26 +158,28 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.post('/', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, validator.bookmarkFolder, apiV3FormValidator, async(req, res) => {
-    const owner = req.user?._id;
-    const { name, parent } = req.body;
-    const params = {
-      name, owner, parent,
-    };
+  router.post('/',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }),
+    loginRequiredStrictly, validator.bookmarkFolder, apiV3FormValidator, async(req, res) => {
+      const owner = req.user?._id;
+      const { name, parent } = req.body;
+      const params = {
+        name, owner, parent,
+      };
 
-    try {
-      const bookmarkFolder = await BookmarkFolder.createByParameters(params);
-      logger.debug('bookmark folder created', bookmarkFolder);
-      return res.apiv3({ bookmarkFolder });
-    }
-    catch (err) {
-      logger.error(err);
-      if (err instanceof InvalidParentBookmarkFolderError) {
-        return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_bookmark_folder'));
+      try {
+        const bookmarkFolder = await BookmarkFolder.createByParameters(params);
+        logger.debug('bookmark folder created', bookmarkFolder);
+        return res.apiv3({ bookmarkFolder });
       }
-      return res.apiv3Err(err, 500);
-    }
-  });
+      catch (err) {
+        logger.error(err);
+        if (err instanceof InvalidParentBookmarkFolderError) {
+          return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_bookmark_folder'));
+        }
+        return res.apiv3Err(err, 500);
+      }
+    });
 
   /**
    * @swagger
@@ -211,7 +213,7 @@ module.exports = (crowi) => {
    *                        type: object
    *                        $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.get('/list/:userId', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK]), loginRequiredStrictly, async(req, res) => {
+  router.get('/list/:userId', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => {
     const { userId } = req.params;
 
     const getBookmarkFolders = async(
@@ -300,7 +302,7 @@ module.exports = (crowi) => {
    *                      description: Number of deleted folders
    *                      example: 1
    */
-  router.delete('/:id', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, async(req, res) => {
+  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);
@@ -357,19 +359,20 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.put('/', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
-    const {
-      bookmarkFolderId, name, parent, childFolder,
-    } = req.body;
-    try {
-      const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent, childFolder);
-      return res.apiv3({ bookmarkFolder });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+  router.put('/',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
+      const {
+        bookmarkFolderId, name, parent, childFolder,
+      } = req.body;
+      try {
+        const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent, childFolder);
+        return res.apiv3({ bookmarkFolder });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err, 500);
+      }
+    });
 
   /**
    * @swagger
@@ -407,7 +410,8 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.post('/add-bookmark-to-folder', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, validator.bookmarkPage, apiV3FormValidator,
+  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 { pageId, folderId } = req.body;
@@ -458,17 +462,18 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.put('/update-bookmark', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, validator.bookmark, async(req, res) => {
-    const { pageId, status } = req.body;
-    const userId = req.user?._id;
-    try {
-      const bookmarkFolder = await BookmarkFolder.updateBookmark(pageId, status, userId);
-      return res.apiv3({ bookmarkFolder });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+  router.put('/update-bookmark',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmark, async(req, res) => {
+      const { pageId, status } = req.body;
+      const userId = req.user?._id;
+      try {
+        const bookmarkFolder = await BookmarkFolder.updateBookmark(pageId, status, userId);
+        return res.apiv3({ bookmarkFolder });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err, 500);
+      }
+    });
   return router;
 };

+ 69 - 65
apps/app/src/server/routes/apiv3/bookmarks.js

@@ -126,43 +126,44 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/BookmarkInfo'
    */
-  router.get('/info', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK]), loginRequired, validator.bookmarkInfo, apiV3FormValidator, async(req, res) => {
-    const { user } = req;
-    const { pageId } = req.query;
+  router.get('/info',
+    accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequired, validator.bookmarkInfo, apiV3FormValidator, async(req, res) => {
+      const { user } = req;
+      const { pageId } = req.query;
 
-    const responsesParams = {};
+      const responsesParams = {};
 
-    try {
-      const bookmarks = await Bookmark.find({ page: pageId }).populate('user');
-      let users = [];
-      if (bookmarks.length > 0) {
-        users = bookmarks.map(bookmark => serializeUserSecurely(bookmark.user));
+      try {
+        const bookmarks = await Bookmark.find({ page: pageId }).populate('user');
+        let users = [];
+        if (bookmarks.length > 0) {
+          users = bookmarks.map(bookmark => serializeUserSecurely(bookmark.user));
+        }
+        responsesParams.sumOfBookmarks = bookmarks.length;
+        responsesParams.bookmarkedUsers = users;
+        responsesParams.pageId = pageId;
+      }
+      catch (err) {
+        logger.error('get-bookmark-document-failed', err);
+        return res.apiv3Err(err, 500);
+      }
+
+      // guest user only get bookmark count
+      if (user == null) {
+        return res.apiv3(responsesParams);
+      }
+
+      try {
+        const bookmark = await Bookmark.findByPageIdAndUserId(pageId, user._id);
+        responsesParams.isBookmarked = (bookmark != null);
+        return res.apiv3(responsesParams);
+      }
+      catch (err) {
+        logger.error('get-bookmark-state-failed', err);
+        return res.apiv3Err(err, 500);
       }
-      responsesParams.sumOfBookmarks = bookmarks.length;
-      responsesParams.bookmarkedUsers = users;
-      responsesParams.pageId = pageId;
-    }
-    catch (err) {
-      logger.error('get-bookmark-document-failed', err);
-      return res.apiv3Err(err, 500);
-    }
-
-    // guest user only get bookmark count
-    if (user == null) {
-      return res.apiv3(responsesParams);
-    }
-
-    try {
-      const bookmark = await Bookmark.findByPageIdAndUserId(pageId, user._id);
-      responsesParams.isBookmarked = (bookmark != null);
-      return res.apiv3(responsesParams);
-    }
-    catch (err) {
-      logger.error('get-bookmark-state-failed', err);
-      return res.apiv3Err(err, 500);
-    }
-
-  });
+
+    });
 
   // select page from bookmark where userid = userid
   /**
@@ -193,36 +194,38 @@ module.exports = (crowi) => {
     param('userId').isMongoId().withMessage('userId is required'),
   ];
 
-  router.get('/:userId', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK]), loginRequired, validator.userBookmarkList, apiV3FormValidator, async(req, res) => {
-    const { userId } = req.params;
-
-    if (userId == null) {
-      return res.apiv3Err('User id is not found or forbidden', 400);
-    }
-    try {
-      const bookmarkIdsInFolders = await BookmarkFolder.distinct('bookmarks', { owner: userId });
-      const userRootBookmarks = await Bookmark.find({
-        _id: { $nin: bookmarkIdsInFolders },
-        user: userId,
-      }).populate({
-        path: 'page',
-        model: 'Page',
-        populate: {
-          path: 'lastUpdateUser',
-          model: 'User',
-        },
-      }).exec();
-
-      // serialize Bookmark
-      const serializedUserRootBookmarks = userRootBookmarks.map(bookmark => serializeBookmarkSecurely(bookmark));
-
-      return res.apiv3({ userRootBookmarks: serializedUserRootBookmarks });
-    }
-    catch (err) {
-      logger.error('get-bookmark-failed', err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+  router.get('/:userId',
+    accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK], { acceptLegacy: true }),
+    loginRequired, validator.userBookmarkList, apiV3FormValidator, async(req, res) => {
+      const { userId } = req.params;
+
+      if (userId == null) {
+        return res.apiv3Err('User id is not found or forbidden', 400);
+      }
+      try {
+        const bookmarkIdsInFolders = await BookmarkFolder.distinct('bookmarks', { owner: userId });
+        const userRootBookmarks = await Bookmark.find({
+          _id: { $nin: bookmarkIdsInFolders },
+          user: userId,
+        }).populate({
+          path: 'page',
+          model: 'Page',
+          populate: {
+            path: 'lastUpdateUser',
+            model: 'User',
+          },
+        }).exec();
+
+        // serialize Bookmark
+        const serializedUserRootBookmarks = userRootBookmarks.map(bookmark => serializeBookmarkSecurely(bookmark));
+
+        return res.apiv3({ userRootBookmarks: serializedUserRootBookmarks });
+      }
+      catch (err) {
+        logger.error('get-bookmark-failed', err);
+        return res.apiv3Err(err, 500);
+      }
+    });
 
 
   /**
@@ -250,7 +253,8 @@ module.exports = (crowi) => {
    *                    bookmark:
    *                      $ref: '#/components/schemas/Bookmark'
    */
-  router.put('/', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, addActivity, validator.bookmarks, apiV3FormValidator,
+  router.put('/',
+    accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, addActivity, validator.bookmarks, apiV3FormValidator,
     async(req, res) => {
       const { pageId, bool } = req.body;
       const userId = req.user?._id;

+ 3 - 3
apps/app/src/server/routes/apiv3/export.js

@@ -173,7 +173,7 @@ module.exports = (crowi) => {
    *                  status:
    *                    $ref: '#/components/schemas/ExportStatus'
    */
-  router.get('/status', accessTokenParser([SCOPE.READ.ADMIN.EXPORET_DATA]), loginRequired, adminRequired, async(req, res) => {
+  router.get('/status', accessTokenParser([SCOPE.READ.ADMIN.EXPORET_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => {
     const status = await exportService.getStatus();
 
     // TODO: use res.apiv3
@@ -214,7 +214,7 @@ module.exports = (crowi) => {
    *                    type: boolean
    *                    description: whether the request is succeeded
    */
-  router.post('/', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA]), loginRequired, adminRequired, addActivity, async(req, res) => {
+  router.post('/', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA], { acceptLegacy: true }), loginRequired, adminRequired, addActivity, async(req, res) => {
     // TODO: add express validator
     try {
       const { collections } = req.body;
@@ -264,7 +264,7 @@ module.exports = (crowi) => {
    *                    type: boolean
    *                    description: whether the request is succeeded
    */
-  router.delete('/:fileName', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA]), loginRequired, adminRequired,
+  router.delete('/:fileName', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA], { acceptLegacy: true }), loginRequired, adminRequired,
     validator.deleteFile, apiV3FormValidator, addActivity,
     async(req, res) => {
     // TODO: add express validator

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

@@ -198,7 +198,6 @@ module.exports = (crowi: Crowi): Router => {
    *                          type: number
    *                          description: The size of the file
    */
-  // eslint-disable-next-line max-len
   receiveRouter.get('/files', validateTransferKey, async(req: Request, res: ApiV3Response) => {
     const files = await crowi.fileUploadService.listFiles();
     return res.apiv3({ files });
@@ -249,7 +248,6 @@ module.exports = (crowi: Crowi): Router => {
    *                    type: string
    *                    description: The message of the result
    */
-  // eslint-disable-next-line max-len
   receiveRouter.post('/', validateTransferKey, uploads.single('transferDataZipFile'), async(req: Request & { file: any; }, res: ApiV3Response) => {
     const { file } = req;
     const {
@@ -466,31 +464,32 @@ module.exports = (crowi: Crowi): Router => {
    *                    type: string
    *                    description: The transfer key
    */
-  // eslint-disable-next-line max-len
-  receiveRouter.post('/generate-key', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA]), adminRequiredIfInstalled, appSiteUrlRequiredIfNotInstalled, async(req: Request, res: ApiV3Response) => {
-    const appSiteUrl = req.body.appSiteUrl ?? configManager.getConfig('app:siteUrl');
+  receiveRouter.post('/generate-key',
+    accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA], { acceptLegacy: true }),
+    adminRequiredIfInstalled, appSiteUrlRequiredIfNotInstalled, async(req: Request, res: ApiV3Response) => {
+      const appSiteUrl = req.body.appSiteUrl ?? configManager.getConfig('app:siteUrl');
 
-    let appSiteUrlOrigin: string;
-    try {
-      appSiteUrlOrigin = new URL(appSiteUrl).origin;
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('appSiteUrl may be wrong', 'failed_to_generate_key_string'));
-    }
+      let appSiteUrlOrigin: string;
+      try {
+        appSiteUrlOrigin = new URL(appSiteUrl).origin;
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3('appSiteUrl may be wrong', 'failed_to_generate_key_string'));
+      }
 
-    // Save TransferKey document
-    let transferKeyString: string;
-    try {
-      transferKeyString = await g2gTransferReceiverService.createTransferKey(appSiteUrlOrigin);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('Error occurred while generating transfer key.', 'failed_to_generate_key'));
-    }
+      // Save TransferKey document
+      let transferKeyString: string;
+      try {
+        transferKeyString = await g2gTransferReceiverService.createTransferKey(appSiteUrlOrigin);
+      }
+      catch (err) {
+        logger.error(err);
+        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
@@ -532,43 +531,44 @@ module.exports = (crowi: Crowi): Router => {
    *                    type: string
    *                    description: The message of the result
    */
-  // eslint-disable-next-line max-len
-  pushRouter.post('/transfer', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA]), loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const { transferKey, collections, optionsMap } = req.body;
+  pushRouter.post('/transfer',
+    accessTokenParser([SCOPE.WRITE.ADMIN.EXPORET_DATA], { acceptLegacy: true }),
+    loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+      const { transferKey, collections, optionsMap } = req.body;
 
-    // Parse transfer key
-    let tk: TransferKey;
-    try {
-      tk = TransferKey.parse(transferKey);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400);
-    }
+      // Parse transfer key
+      let tk: TransferKey;
+      try {
+        tk = TransferKey.parse(transferKey);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400);
+      }
 
-    // get growi info
-    let destGROWIInfo: IDataGROWIInfo;
-    try {
-      destGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI info.', 'failed_to_ask_growi_info'));
-    }
+      // get growi info
+      let destGROWIInfo: IDataGROWIInfo;
+      try {
+        destGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI info.', 'failed_to_ask_growi_info'));
+      }
 
-    // Check if can transfer
-    const transferability = await g2gTransferPusherService.getTransferability(destGROWIInfo);
-    if (!transferability.canTransfer) {
-      return res.apiv3Err(new ErrorV3(transferability.reason, 'growi_incompatible_to_transfer'));
-    }
+      // Check if can transfer
+      const transferability = await g2gTransferPusherService.getTransferability(destGROWIInfo);
+      if (!transferability.canTransfer) {
+        return res.apiv3Err(new ErrorV3(transferability.reason, 'growi_incompatible_to_transfer'));
+      }
 
-    // Start transfer
-    // DO NOT "await". Let it run in the background.
-    // Errors should be emitted through websocket.
-    g2gTransferPusherService.startTransfer(tk, req.user, collections, optionsMap, destGROWIInfo);
+      // Start transfer
+      // DO NOT "await". Let it run in the background.
+      // Errors should be emitted through websocket.
+      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
   router.use(receiveRouter, pushRouter);

+ 6 - 5
apps/app/src/server/routes/apiv3/import.js

@@ -199,7 +199,7 @@ export default function route(crowi) {
    *                        type: string
    *                        description: the access token of qiita.com
    */
-  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA]), loginRequired, adminRequired, async(req, res) => {
+  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'),
@@ -238,7 +238,7 @@ export default function route(crowi) {
    *                  status:
    *                    $ref: '#/components/schemas/ImportStatus'
    */
-  router.get('/status', accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA]), loginRequired, adminRequired, async(req, res) => {
+  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);
@@ -286,7 +286,7 @@ export default function route(crowi) {
    *        200:
    *          description: Import process has requested
    */
-  router.post('/', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequired, adminRequired, addActivity, async(req, res) => {
+  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;
 
@@ -409,7 +409,8 @@ export default function route(crowi) {
    *              schema:
    *                $ref: '#/components/schemas/FileImportResponse'
    */
-  router.post('/upload', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequired, adminRequired, uploads.single('file'), addActivity,
+  router.post('/upload',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, uploads.single('file'), addActivity,
     async(req, res) => {
       const { file } = req;
       const zipFile = importService.getFile(file.filename);
@@ -455,7 +456,7 @@ export default function route(crowi) {
    *        200:
    *          description: all files are deleted
    */
-  router.delete('/all', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequired, adminRequired, async(req, res) => {
+  router.delete('/all', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => {
     try {
       importService.deleteAllZipFiles();
 

+ 5 - 4
apps/app/src/server/routes/apiv3/in-app-notification.ts

@@ -134,7 +134,7 @@ module.exports = (crowi) => {
    *              schema:
    *                $ref: '#/components/schemas/InAppNotificationListResponse'
    */
-  router.get('/list', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly,
+  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
@@ -214,7 +214,7 @@ module.exports = (crowi) => {
    *                    type: integer
    *                    description: Count of unread notifications
    */
-  router.get('/status', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly,
+  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
@@ -259,7 +259,7 @@ module.exports = (crowi) => {
    *              schema:
    *                type: object
    */
-  router.post('/open', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly,
+  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
@@ -293,7 +293,8 @@ module.exports = (crowi) => {
    *        200:
    *          description: All notifications opened successfully
    */
-  router.put('/all-statuses-open', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly, addActivity,
+  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

+ 111 - 107
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -89,19 +89,20 @@ const routerFactory = (crowi: Crowi): Router => {
    *                 rootPage:
    *                   $ref: '#/components/schemas/Page'
    */
-  router.get('/root', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const Page = mongoose.model<IPage, PageModel>('Page');
+  router.get('/root',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+      const Page = mongoose.model<IPage, PageModel>('Page');
 
-    let rootPage;
-    try {
-      rootPage = await Page.findByPathAndViewer('/', req.user, null, true);
-    }
-    catch (err) {
-      return res.apiv3Err(new ErrorV3('rootPage not found'));
-    }
+      let rootPage;
+      try {
+        rootPage = await Page.findByPathAndViewer('/', req.user, null, true);
+      }
+      catch (err) {
+        return res.apiv3Err(new ErrorV3('rootPage not found'));
+      }
 
-    return res.apiv3({ rootPage });
-  });
+      return res.apiv3({ rootPage });
+    });
 
   /**
    * @swagger
@@ -153,21 +154,22 @@ const routerFactory = (crowi: Crowi): Router => {
    *                         nullable: true
    *                         description: Revision ID (nullable)
    */
-  // eslint-disable-next-line max-len
-  router.get('/ancestors-children', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
-    const { path } = req.query;
-
-    const pageService = crowi.pageService;
-    try {
-      const ancestorsChildren = await pageService.findAncestorsChildrenByPathAndViewer(path as string, req.user);
-      return res.apiv3({ ancestorsChildren });
-    }
-    catch (err) {
-      logger.error('Failed to get ancestorsChildren.', err);
-      return res.apiv3Err(new ErrorV3('Failed to get ancestorsChildren.'));
-    }
-
-  });
+  router.get('/ancestors-children',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+      const { path } = req.query;
+
+      const pageService = crowi.pageService;
+      try {
+        const ancestorsChildren = await pageService.findAncestorsChildrenByPathAndViewer(path as string, req.user);
+        return res.apiv3({ ancestorsChildren });
+      }
+      catch (err) {
+        logger.error('Failed to get ancestorsChildren.', err);
+        return res.apiv3Err(new ErrorV3('Failed to get ancestorsChildren.'));
+      }
+
+    });
 
   /**
    * @swagger
@@ -205,26 +207,27 @@ const routerFactory = (crowi: Crowi): Router => {
   /*
    * In most cases, using id should be prioritized
    */
-  // eslint-disable-next-line max-len
-  router.get('/children', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const { id, path } = req.query;
+  router.get('/children',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+      const { id, path } = req.query;
 
-    const pageService = crowi.pageService;
+      const pageService = crowi.pageService;
 
-    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 {
-      const pages = await pageService.findChildrenByParentPathOrIdAndViewer(
+      try {
+        const pages = await pageService.findChildrenByParentPathOrIdAndViewer(
         (id || path) as string, req.user, undefined, !hideRestrictedByOwner, !hideRestrictedByGroup,
-      );
-      return res.apiv3({ children: pages });
-    }
-    catch (err) {
-      logger.error('Error occurred while finding children.', err);
-      return res.apiv3Err(new ErrorV3('Error occurred while finding children.'));
-    }
-  });
+        );
+        return res.apiv3({ children: pages });
+      }
+      catch (err) {
+        logger.error('Error occurred while finding children.', err);
+        return res.apiv3Err(new ErrorV3('Error occurred while finding children.'));
+      }
+    });
 
   /**
    * @swagger
@@ -300,79 +303,80 @@ const routerFactory = (crowi: Crowi): Router => {
    *                       sumOfSeenUsers:
    *                         type: integer
    */
-  // eslint-disable-next-line max-len
-  router.get('/info', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, validator.pageIdsOrPathRequired, validator.infoParams, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const {
-      pageIds, path, attachBookmarkCount: attachBookmarkCountParam, attachShortBody: attachShortBodyParam,
-    } = req.query;
-
-    const attachBookmarkCount: boolean = attachBookmarkCountParam === 'true';
-    const attachShortBody: boolean = attachShortBodyParam === 'true';
-
-    const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const Bookmark = mongoose.model<any, any>('Bookmark');
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const pageService = crowi.pageService;
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const pageGrantService: IPageGrantService = crowi.pageGrantService!;
-
-    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 foundIds = pages.map(page => page._id);
-
-      let shortBodiesMap;
-      if (attachShortBody) {
-        shortBodiesMap = await pageService.shortBodiesMapByPageIds(foundIds, req.user);
-      }
+  router.get('/info',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired, validator.pageIdsOrPathRequired, validator.infoParams, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+      const {
+        pageIds, path, attachBookmarkCount: attachBookmarkCountParam, attachShortBody: attachShortBodyParam,
+      } = req.query;
 
-      let bookmarkCountMap;
-      if (attachBookmarkCount) {
-        bookmarkCountMap = await Bookmark.getPageIdToCountMap(foundIds) as Record<string, number>;
-      }
+      const attachBookmarkCount: boolean = attachBookmarkCountParam === 'true';
+      const attachShortBody: boolean = attachShortBodyParam === 'true';
 
-      const idToPageInfoMap: Record<string, IPageInfo | IPageInfoForListing> = {};
+      const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const Bookmark = mongoose.model<any, any>('Bookmark');
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      const pageService = crowi.pageService;
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      const pageGrantService: IPageGrantService = crowi.pageGrantService!;
 
-      const isGuestUser = req.user == null;
+      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 userRelatedGroups = await pageGrantService.getUserRelatedGroups(req.user);
+        const foundIds = pages.map(page => page._id);
 
-      for (const page of pages) {
+        let shortBodiesMap;
+        if (attachShortBody) {
+          shortBodiesMap = await pageService.shortBodiesMapByPageIds(foundIds, req.user);
+        }
+
+        let bookmarkCountMap;
+        if (attachBookmarkCount) {
+          bookmarkCountMap = await Bookmark.getPageIdToCountMap(foundIds) as Record<string, number>;
+        }
+
+        const idToPageInfoMap: Record<string, IPageInfo | IPageInfoForListing> = {};
+
+        const isGuestUser = req.user == null;
+
+        const userRelatedGroups = await pageGrantService.getUserRelatedGroups(req.user);
+
+        for (const page of pages) {
         // construct isIPageInfoForListing
-        const basicPageInfo = pageService.constructBasicPageInfo(page, isGuestUser);
-
-        // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574)
-        const canDeleteCompletely = pageService.canDeleteCompletely(
-          page,
-          page.creator == null ? null : getIdForRef(page.creator),
-          req.user,
-          false,
-          userRelatedGroups,
-        ); // use normal delete config
-
-        const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
-          ? basicPageInfo
+          const basicPageInfo = pageService.constructBasicPageInfo(page, isGuestUser);
+
+          // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574)
+          const canDeleteCompletely = pageService.canDeleteCompletely(
+            page,
+            page.creator == null ? null : getIdForRef(page.creator),
+            req.user,
+            false,
+            userRelatedGroups,
+          ); // use normal delete config
+
+          const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
+            ? basicPageInfo
           // create IPageInfoForListing
-          : {
-            ...basicPageInfo,
-            isAbleToDeleteCompletely: canDeleteCompletely,
-            bookmarkCount: bookmarkCountMap != null ? bookmarkCountMap[page._id.toString()] : undefined,
-            revisionShortBody: shortBodiesMap != null ? shortBodiesMap[page._id.toString()] : undefined,
-          } as IPageInfoForListing;
-
-        idToPageInfoMap[page._id.toString()] = pageInfo;
-      }
+            : {
+              ...basicPageInfo,
+              isAbleToDeleteCompletely: canDeleteCompletely,
+              bookmarkCount: bookmarkCountMap != null ? bookmarkCountMap[page._id.toString()] : undefined,
+              revisionShortBody: shortBodiesMap != null ? shortBodiesMap[page._id.toString()] : undefined,
+            } as IPageInfoForListing;
+
+          idToPageInfoMap[page._id.toString()] = pageInfo;
+        }
 
-      return res.apiv3(idToPageInfoMap);
-    }
-    catch (err) {
-      logger.error('Error occurred while fetching page informations.', err);
-      return res.apiv3Err(new ErrorV3('Error occurred while fetching page informations.'));
-    }
-  });
+        return res.apiv3(idToPageInfoMap);
+      }
+      catch (err) {
+        logger.error('Error occurred while fetching page informations.', err);
+        return res.apiv3Err(new ErrorV3('Error occurred while fetching page informations.'));
+      }
+    });
 
   return router;
 };

+ 1 - 1
apps/app/src/server/routes/apiv3/page/check-page-existence.ts

@@ -40,7 +40,7 @@ export const checkPageExistenceHandlersFactory: CreatePageHandlersFactory = (cro
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequired,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequired,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { path } = req.query;

+ 1 - 1
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -218,7 +218,7 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
   const addActivity = generateAddActivityMiddleware();
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
     validator, apiV3FormValidator,
     async(req: CreatePageRequest, res: ApiV3Response) => {
       const {

+ 1 - 1
apps/app/src/server/routes/apiv3/page/get-page-paths-with-descendant-count.ts

@@ -57,7 +57,7 @@ export const getPagePathsWithDescendantCountFactory: GetPagePathsWithDescendantC
   ];
 
   return [
-    accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const {

+ 1 - 1
apps/app/src/server/routes/apiv3/page/get-yjs-data.ts

@@ -35,7 +35,7 @@ export const getYjsDataHandlerFactory: GetYjsDataHandlerFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;

+ 50 - 48
apps/app/src/server/routes/apiv3/page/index.ts

@@ -213,61 +213,63 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Page'
    */
-  router.get('/', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), certifySharedPage, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
-    const { user, isSharedPage } = req;
-    const {
-      pageId, path, findAll, revisionId, shareLinkId, includeEmpty,
-    } = req.query;
+  router.get('/',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    certifySharedPage, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
+      const { user, isSharedPage } = req;
+      const {
+        pageId, path, findAll, revisionId, shareLinkId, includeEmpty,
+      } = req.query;
 
-    const isValid = (shareLinkId != null && pageId != null && path == null) || (shareLinkId == null && (pageId != null || path != null));
-    if (!isValid) {
-      return res.apiv3Err(new Error('Either parameter of (pageId or path) or (pageId and shareLinkId) is required.'), 400);
-    }
+      const isValid = (shareLinkId != null && pageId != null && path == null) || (shareLinkId == null && (pageId != null || path != null));
+      if (!isValid) {
+        return res.apiv3Err(new Error('Either parameter of (pageId or path) or (pageId and shareLinkId) is required.'), 400);
+      }
 
-    let page;
-    let pages;
-    try {
-      if (isSharedPage) {
-        const shareLink = await ShareLink.findOne({ _id: shareLinkId });
-        if (shareLink == null) {
-          throw new Error('ShareLink is not found');
+      let page;
+      let pages;
+      try {
+        if (isSharedPage) {
+          const shareLink = await ShareLink.findOne({ _id: shareLinkId });
+          if (shareLink == null) {
+            throw new Error('ShareLink is not found');
+          }
+          page = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
+        }
+        else if (pageId != null) { // prioritized
+          page = await Page.findByIdAndViewer(pageId, user);
+        }
+        else if (!findAll) {
+          page = await Page.findByPathAndViewer(path, user, null, true, false);
+        }
+        else {
+          pages = await Page.findByPathAndViewer(path, user, null, false, includeEmpty);
         }
-        page = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
-      }
-      else if (pageId != null) { // prioritized
-        page = await Page.findByIdAndViewer(pageId, user);
-      }
-      else if (!findAll) {
-        page = await Page.findByPathAndViewer(path, user, null, true, false);
       }
-      else {
-        pages = await Page.findByPathAndViewer(path, user, null, false, includeEmpty);
+      catch (err) {
+        logger.error('get-page-failed', err);
+        return res.apiv3Err(err, 500);
       }
-    }
-    catch (err) {
-      logger.error('get-page-failed', err);
-      return res.apiv3Err(err, 500);
-    }
 
-    if (page == null && (pages == null || pages.length === 0)) {
-      return res.apiv3Err('Page is not found', 404);
-    }
+      if (page == null && (pages == null || pages.length === 0)) {
+        return res.apiv3Err('Page is not found', 404);
+      }
 
-    if (page != null) {
-      try {
-        page.initLatestRevisionField(revisionId);
+      if (page != null) {
+        try {
+          page.initLatestRevisionField(revisionId);
 
-        // populate
-        page = await page.populateDataToShowRevision();
-      }
-      catch (err) {
-        logger.error('populate-page-failed', err);
-        return res.apiv3Err(err, 500);
+          // populate
+          page = await page.populateDataToShowRevision();
+        }
+        catch (err) {
+          logger.error('populate-page-failed', err);
+          return res.apiv3Err(err, 500);
+        }
       }
-    }
 
-    return res.apiv3({ page, pages });
-  });
+      return res.apiv3({ page, pages });
+    });
 
   router.get('/page-paths-with-descendant-count', getPagePathsWithDescendantCountFactory(crowi));
 
@@ -441,7 +443,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Page'
    */
-  router.put('/likes', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, addActivity,
+  router.put('/likes', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, addActivity,
     validator.likes, apiV3FormValidator, async(req, res) => {
       const { pageId, bool: isLiked } = req.body;
 
@@ -1037,7 +1039,7 @@ module.exports = (crowi) => {
    *          500:
    *            description: Internal server error.
    */
-  router.put('/subscribe', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, addActivity,
+  router.put('/subscribe', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, addActivity,
     validator.subscribe, apiV3FormValidator,
     async(req, res) => {
       const { pageId, status } = req.body;
@@ -1099,7 +1101,7 @@ module.exports = (crowi) => {
    *                   page:
    *                     $ref: '#/components/schemas/Page'
    */
-  router.put('/:pageId/content-width', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, excludeReadOnlyUser,
+  router.put('/:pageId/content-width', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, excludeReadOnlyUser,
     validator.contentWidth, apiV3FormValidator, async(req, res) => {
       const { pageId } = req.params;
       const { expandContentWidth } = req.body;

+ 1 - 1
apps/app/src/server/routes/apiv3/page/publish-page.ts

@@ -39,7 +39,7 @@ export const publishPageHandlersFactory: PublishPageHandlersFactory = (crowi) =>
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;

+ 1 - 1
apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts

@@ -40,7 +40,7 @@ export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionB
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;

+ 1 - 1
apps/app/src/server/routes/apiv3/page/unpublish-page.ts

@@ -39,7 +39,7 @@ export const unpublishPageHandlersFactory: UnpublishPageHandlersFactory = (crowi
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;

+ 1 - 1
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -134,7 +134,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const addActivity = generateAddActivityMiddleware();
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
     validator, apiV3FormValidator,
     async(req: UpdatePageRequest, res: ApiV3Response) => {
       const {

+ 95 - 95
apps/app/src/server/routes/apiv3/pages/index.js

@@ -157,69 +157,70 @@ module.exports = (crowi) => {
    *          200:
    *            description: Return pages recently updated
    */
-  router.get('/recent', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, validator.recent, apiV3FormValidator, async(req, res) => {
-    const limit = parseInt(req.query.limit) || 20;
-    const offset = parseInt(req.query.offset) || 0;
-    const includeWipPage = req.query.includeWipPage === 'true'; // Need validation using express-validator
+  router.get('/recent', accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired, validator.recent, apiV3FormValidator, async(req, res) => {
+      const limit = parseInt(req.query.limit) || 20;
+      const offset = parseInt(req.query.offset) || 0;
+      const includeWipPage = req.query.includeWipPage === 'true'; // Need validation using express-validator
 
-    const hideRestrictedByOwner = configManager.getConfig('security:list-policy:hideRestrictedByOwner');
-    const hideRestrictedByGroup = configManager.getConfig('security:list-policy:hideRestrictedByGroup');
+      const hideRestrictedByOwner = configManager.getConfig('security:list-policy:hideRestrictedByOwner');
+      const hideRestrictedByGroup = configManager.getConfig('security:list-policy:hideRestrictedByGroup');
 
-    /**
+      /**
     * @type {import('~/server/models/page').FindRecentUpdatedPagesOption}
     */
-    const queryOptions = {
-      offset,
-      limit,
-      includeWipPage,
-      includeTrashed: false,
-      isRegExpEscapedFromPath: true,
-      sort: 'updatedAt',
-      desc: -1,
-      hideRestrictedByOwner,
-      hideRestrictedByGroup,
-    };
-
-    try {
-      const result = await Page.findRecentUpdatedPages('/', req.user, queryOptions);
-      if (result.pages.length > limit) {
-        result.pages.pop();
-      }
+      const queryOptions = {
+        offset,
+        limit,
+        includeWipPage,
+        includeTrashed: false,
+        isRegExpEscapedFromPath: true,
+        sort: 'updatedAt',
+        desc: -1,
+        hideRestrictedByOwner,
+        hideRestrictedByGroup,
+      };
 
-      result.pages.forEach((page) => {
-        if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
-          page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
-        }
-      });
-
-      const ids = result.pages.map((page) => { return page._id });
-      const relations = await PageTagRelation.find({ relatedPage: { $in: ids } }).populate('relatedTag');
-
-      // { pageId: [{ tag }, ...] }
-      const relationsMap = new Map();
-      // increment relationsMap
-      relations.forEach((relation) => {
-        const pageId = relation.relatedPage.toString();
-        if (!relationsMap.has(pageId)) {
-          relationsMap.set(pageId, []);
-        }
-        if (relation.relatedTag != null) {
-          relationsMap.get(pageId).push(relation.relatedTag);
+      try {
+        const result = await Page.findRecentUpdatedPages('/', req.user, queryOptions);
+        if (result.pages.length > limit) {
+          result.pages.pop();
         }
-      });
-      // add tags to each page
-      result.pages.forEach((page) => {
-        const pageId = page._id.toString();
-        page.tags = relationsMap.has(pageId) ? relationsMap.get(pageId) : [];
-      });
 
-      return res.apiv3(result);
-    }
-    catch (err) {
-      logger.error('Failed to get recent pages', err);
-      return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
-    }
-  });
+        result.pages.forEach((page) => {
+          if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+            page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+          }
+        });
+
+        const ids = result.pages.map((page) => { return page._id });
+        const relations = await PageTagRelation.find({ relatedPage: { $in: ids } }).populate('relatedTag');
+
+        // { pageId: [{ tag }, ...] }
+        const relationsMap = new Map();
+        // increment relationsMap
+        relations.forEach((relation) => {
+          const pageId = relation.relatedPage.toString();
+          if (!relationsMap.has(pageId)) {
+            relationsMap.set(pageId, []);
+          }
+          if (relation.relatedTag != null) {
+            relationsMap.get(pageId).push(relation.relatedTag);
+          }
+        });
+        // add tags to each page
+        result.pages.forEach((page) => {
+          const pageId = page._id.toString();
+          page.tags = relationsMap.has(pageId) ? relationsMap.get(pageId) : [];
+        });
+
+        return res.apiv3(result);
+      }
+      catch (err) {
+        logger.error('Failed to get recent pages', err);
+        return res.apiv3Err(new ErrorV3('Failed to get recent pages', 'unknown'), 500);
+      }
+    });
 
   /**
    * @swagger
@@ -275,7 +276,7 @@ module.exports = (crowi) => {
    */
   router.put(
     '/rename',
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     validator.renamePage,
@@ -384,7 +385,7 @@ module.exports = (crowi) => {
     */
   router.post(
     '/resume-rename',
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequiredStrictly,
     validator.resumeRenamePage,
     apiV3FormValidator,
@@ -440,7 +441,7 @@ module.exports = (crowi) => {
    */
   router.delete(
     '/empty-trash',
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequired,
     excludeReadOnlyUser,
     addActivity,
@@ -552,40 +553,41 @@ module.exports = (crowi) => {
     *                              lastUpdateUser:
     *                                $ref: '#/components/schemas/User'
     */
-  router.get('/list', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, validator.list, apiV3FormValidator, async(req, res) => {
+  router.get('/list', accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired, validator.list, apiV3FormValidator, async(req, res) => {
 
-    const path = normalizePath(req.query.path ?? '/');
-    const limit = parseInt(req.query.limit ?? configManager.getConfig('customize:showPageLimitationS'));
-    const page = req.query.page || 1;
-    const offset = (page - 1) * limit;
-    let includeTrashed = false;
+      const path = normalizePath(req.query.path ?? '/');
+      const limit = parseInt(req.query.limit ?? configManager.getConfig('customize:showPageLimitationS'));
+      const page = req.query.page || 1;
+      const offset = (page - 1) * limit;
+      let includeTrashed = false;
 
-    if (isTrashPage(path)) {
-      includeTrashed = true;
-    }
+      if (isTrashPage(path)) {
+        includeTrashed = true;
+      }
 
-    const queryOptions = {
-      offset,
-      limit,
-      includeTrashed,
-    };
+      const queryOptions = {
+        offset,
+        limit,
+        includeTrashed,
+      };
 
-    try {
-      const result = await Page.findListWithDescendants(path, req.user, queryOptions);
+      try {
+        const result = await Page.findListWithDescendants(path, req.user, queryOptions);
 
-      result.pages.forEach((page) => {
-        if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
-          page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
-        }
-      });
+        result.pages.forEach((page) => {
+          if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+            page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+          }
+        });
 
-      return res.apiv3(result);
-    }
-    catch (err) {
-      logger.error('Failed to get Descendants Pages', err);
-      return res.apiv3Err(err, 500);
-    }
-  });
+        return res.apiv3(result);
+      }
+      catch (err) {
+        logger.error('Failed to get Descendants Pages', err);
+        return res.apiv3Err(err, 500);
+      }
+    });
 
   /**
    * @swagger
@@ -630,7 +632,7 @@ module.exports = (crowi) => {
    */
   router.post(
     '/duplicate',
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     addActivity,
@@ -735,7 +737,7 @@ module.exports = (crowi) => {
    */
   router.get(
     '/subordinated-list',
-    accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequired,
     async(req, res) => {
       const { path } = req.query;
@@ -799,7 +801,7 @@ module.exports = (crowi) => {
     */
   router.post(
     '/delete',
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     validator.deletePages,
@@ -885,10 +887,9 @@ module.exports = (crowi) => {
    *                  type: object
    *                  description: Empty object
    */
-  // eslint-disable-next-line max-len
   router.post(
     '/convert-pages-by-path',
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     adminRequired,
@@ -946,10 +947,9 @@ module.exports = (crowi) => {
    *                  type: object
    *                  description: Empty object
   */
-  // eslint-disable-next-line max-len
   router.post(
     '/legacy-pages-migration',
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequired,
     excludeReadOnlyUser,
     validator.legacyPagesMigration,
@@ -1004,7 +1004,7 @@ module.exports = (crowi) => {
    *                      type: number
    *                      description: Number of pages that can be migrated
    */
-  router.get('/v5-migration-status', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, async(req, res) => {
+  router.get('/v5-migration-status', accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, async(req, res) => {
     try {
       const isV5Compatible = configManager.getConfig('app:isV5Compatible');
       const migratablePagesCount = req.user != null ? await crowi.pageService.countPagesCanNormalizeParentByUser(req.user) : null; // null check since not using loginRequiredStrictly

+ 54 - 50
apps/app/src/server/routes/apiv3/personal-setting/index.js

@@ -154,7 +154,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: personal params
    */
-  router.get('/', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequiredStrictly, async(req, res) => {
+  router.get('/', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => {
     const { username } = req.user;
     try {
       const user = await User.findUserByUsername(username);
@@ -196,7 +196,7 @@ module.exports = (crowi) => {
    *                      type: number
    *                      description: Minimum password length
    */
-  router.get('/is-password-set', accessTokenParser([SCOPE.READ.USER_SETTINGS.PASSWORD]), loginRequiredStrictly, async(req, res) => {
+  router.get('/is-password-set', accessTokenParser([SCOPE.READ.USER_SETTINGS.PASSWORD], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => {
     const { username } = req.user;
 
     try {
@@ -238,7 +238,8 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: personal params
    */
-  router.put('/', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.INFO]), loginRequiredStrictly, addActivity, validator.personal, apiV3FormValidator,
+  router.put('/',
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.INFO], { acceptLegacy: true }), loginRequiredStrictly, addActivity, validator.personal, apiV3FormValidator,
     async(req, res) => {
 
       try {
@@ -299,7 +300,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: user data
    */
-  router.put('/image-type', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.INFO]), loginRequiredStrictly, addActivity,
+  router.put('/image-type', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.INFO], { acceptLegacy: true }), loginRequiredStrictly, addActivity,
     validator.imageType, apiV3FormValidator,
     async(req, res) => {
       const { isGravatarEnabled } = req.body;
@@ -338,19 +339,20 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: array of external accounts
    */
-  router.get('/external-accounts', accessTokenParser([SCOPE.READ.USER_SETTINGS.EXTERNAL_ACCOUNT]), loginRequiredStrictly, async(req, res) => {
-    const userData = req.user;
+  router.get('/external-accounts',
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.EXTERNAL_ACCOUNT], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => {
+      const userData = req.user;
 
-    try {
-      const externalAccounts = await ExternalAccount.find({ user: userData });
-      return res.apiv3({ externalAccounts });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err('get-external-accounts-failed');
-    }
+      try {
+        const externalAccounts = await ExternalAccount.find({ user: userData });
+        return res.apiv3({ externalAccounts });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err('get-external-accounts-failed');
+      }
 
-  });
+    });
 
   /**
    * @swagger
@@ -383,7 +385,8 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: user data updated
    */
-  router.put('/password', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.PASSWORD]), loginRequiredStrictly, addActivity, validator.password, apiV3FormValidator,
+  router.put('/password',
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.PASSWORD], { acceptLegacy: true }), loginRequiredStrictly, addActivity, validator.password, apiV3FormValidator,
     async(req, res) => {
       const { body, user } = req;
       const { oldPassword, newPassword } = body;
@@ -614,8 +617,8 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: Ldap account disassociate to me
    */
-  // eslint-disable-next-line max-len
-  router.put('/disassociate-ldap', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.EXTERNAL_ACCOUNT]), loginRequiredStrictly, addActivity, validator.disassociateLdap, apiV3FormValidator,
+  router.put('/disassociate-ldap',
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.EXTERNAL_ACCOUNT]), loginRequiredStrictly, addActivity, validator.disassociateLdap, apiV3FormValidator,
     async(req, res) => {
       const { user, body } = req;
       const { providerType, accountId } = body;
@@ -770,29 +773,30 @@ module.exports = (crowi) => {
    *                schema:
    *                 type: object
    */
-  // eslint-disable-next-line max-len
-  router.put('/in-app-notification-settings', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly, addActivity, validator.inAppNotificationSettings, apiV3FormValidator, async(req, res) => {
-    const query = { userId: req.user.id };
-    const subscribeRules = req.body.subscribeRules;
+  router.put('/in-app-notification-settings',
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION]),
+    loginRequiredStrictly, addActivity, validator.inAppNotificationSettings, apiV3FormValidator, async(req, res) => {
+      const query = { userId: req.user.id };
+      const subscribeRules = req.body.subscribeRules;
 
-    if (subscribeRules == null) {
-      return res.apiv3Err('no-rules-found');
-    }
+      if (subscribeRules == null) {
+        return res.apiv3Err('no-rules-found');
+      }
 
-    const options = { upsert: true, new: true, runValidators: true };
-    try {
-      const response = await InAppNotificationSettings.findOneAndUpdate(query, { $set: { subscribeRules } }, options);
+      const options = { upsert: true, new: true, runValidators: true };
+      try {
+        const response = await InAppNotificationSettings.findOneAndUpdate(query, { $set: { subscribeRules } }, options);
 
-      const parameters = { action: SupportedAction.ACTION_USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
+        const parameters = { action: SupportedAction.ACTION_USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
 
-      return res.apiv3(response);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err('updating-in-app-notification-settings-failed');
-    }
-  });
+        return res.apiv3(response);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err('updating-in-app-notification-settings-failed');
+      }
+    });
 
   /**
    * @swagger
@@ -854,20 +858,20 @@ module.exports = (crowi) => {
    *                   isQuestionnaireEnabled:
    *                     type: boolean
    */
-  // eslint-disable-next-line max-len
-  router.put('/questionnaire-settings', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequiredStrictly, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
-    const { isQuestionnaireEnabled } = req.body;
-    const { user } = req;
-    try {
-      await user.updateIsQuestionnaireEnabled(isQuestionnaireEnabled);
+  router.put('/questionnaire-settings',
+    accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequiredStrictly, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
+      const { isQuestionnaireEnabled } = req.body;
+      const { user } = req;
+      try {
+        await user.updateIsQuestionnaireEnabled(isQuestionnaireEnabled);
 
-      return res.apiv3({ message: 'Successfully updated questionnaire settings.', isQuestionnaireEnabled });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err({ error: 'Failed to update questionnaire settings.' });
-    }
-  });
+        return res.apiv3({ message: 'Successfully updated questionnaire settings.', isQuestionnaireEnabled });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err({ error: 'Failed to update questionnaire settings.' });
+      }
+    });
 
 
   return router;

+ 4 - 2
apps/app/src/server/routes/apiv3/revisions.js

@@ -135,7 +135,8 @@ module.exports = (crowi) => {
    *                    type: number
    *                    description: offset of the revisions
    */
-  router.get('/list', certifySharedPage, accessTokenParser(SCOPE.READ.FEATURES.PAGE), loginRequired, validator.retrieveRevisions, apiV3FormValidator,
+  router.get('/list',
+    certifySharedPage, accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, validator.retrieveRevisions, apiV3FormValidator,
     async(req, res) => {
       const pageId = req.query.pageId;
       const limit = req.query.limit || await crowi.configManager.getConfig('customize:showPageLimitationS') || 10;
@@ -235,7 +236,8 @@ module.exports = (crowi) => {
    *                    revision:
    *                      $ref: '#/components/schemas/Revision'
    */
-  router.get('/:id', certifySharedPage, accessTokenParser(SCOPE.READ.FEATURES.PAGE), loginRequired, validator.retrieveRevisionById, apiV3FormValidator,
+  router.get('/:id',
+    certifySharedPage, accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, validator.retrieveRevisionById, apiV3FormValidator,
     async(req, res) => {
       const revisionId = req.params.id;
       const pageId = req.query.pageId;

+ 32 - 30
apps/app/src/server/routes/apiv3/search.js

@@ -126,21 +126,22 @@ module.exports = (crowi) => {
    *                    description: Status of indices
    *                    $ref: '#/components/schemas/Indices'
    */
-  router.get('/indices', noCache(), accessTokenParser([SCOPE.READ.ADMIN.FULL_TEXT_SEARCH]), loginRequired, adminRequired, async(req, res) => {
-    const { searchService } = crowi;
-
-    if (!searchService.isConfigured) {
-      return res.apiv3Err(new ErrorV3('SearchService is not configured', 'search-service-unconfigured'), 503);
-    }
-
-    try {
-      const info = await searchService.getInfoForAdmin();
-      return res.status(200).send({ info });
-    }
-    catch (err) {
-      return res.apiv3Err(err, 503);
-    }
-  });
+  router.get('/indices',
+    noCache(), accessTokenParser([SCOPE.READ.ADMIN.FULL_TEXT_SEARCH], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => {
+      const { searchService } = crowi;
+
+      if (!searchService.isConfigured) {
+        return res.apiv3Err(new ErrorV3('SearchService is not configured', 'search-service-unconfigured'), 503);
+      }
+
+      try {
+        const info = await searchService.getInfoForAdmin();
+        return res.status(200).send({ info });
+      }
+      catch (err) {
+        return res.apiv3Err(err, 503);
+      }
+    });
 
   /**
    * @swagger
@@ -154,24 +155,25 @@ module.exports = (crowi) => {
    *        200:
    *          description: Successfully connected
    */
-  router.post('/connection', accessTokenParser([SCOPE.WRITE.ADMIN.FULL_TEXT_SEARCH]), loginRequired, adminRequired, addActivity, async(req, res) => {
-    const { searchService } = crowi;
+  router.post('/connection',
+    accessTokenParser([SCOPE.WRITE.ADMIN.FULL_TEXT_SEARCH], { acceptLegacy: true }), loginRequired, adminRequired, addActivity, async(req, res) => {
+      const { searchService } = crowi;
 
-    if (!searchService.isConfigured) {
-      return res.apiv3Err(new ErrorV3('SearchService is not configured', 'search-service-unconfigured'));
-    }
+      if (!searchService.isConfigured) {
+        return res.apiv3Err(new ErrorV3('SearchService is not configured', 'search-service-unconfigured'));
+      }
 
-    try {
-      await searchService.reconnectClient();
+      try {
+        await searchService.reconnectClient();
 
-      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SEARCH_CONNECTION });
+        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SEARCH_CONNECTION });
 
-      return res.status(200).send();
-    }
-    catch (err) {
-      return res.apiv3Err(err, 503);
-    }
-  });
+        return res.status(200).send();
+      }
+      catch (err) {
+        return res.apiv3Err(err, 503);
+      }
+    });
 
   const validatorForPutIndices = [
     body('operation').isString().isIn(['rebuild', 'normalize']),
@@ -208,7 +210,7 @@ module.exports = (crowi) => {
    *                    type: string
    *                    description: Operation is successfully processed, or requested
    */
-  router.put('/indices', accessTokenParser([SCOPE.WRITE.ADMIN.FULL_TEXT_SEARCH]), loginRequired, adminRequired, addActivity,
+  router.put('/indices', accessTokenParser([SCOPE.WRITE.ADMIN.FULL_TEXT_SEARCH], { acceptLegacy: true }), loginRequired, adminRequired, addActivity,
     validatorForPutIndices, apiV3FormValidator,
     async(req, res) => {
       const operation = req.body.operation;

+ 126 - 120
apps/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -199,7 +199,7 @@ module.exports = (crowi) => {
    *                    errorCode:
    *                      type: string
    */
-  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.SLACK_INTEGRATION]), loginRequiredStrictly, adminRequired, async(req, res) => {
+  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.SLACK_INTEGRATION], { acceptLegacy: true }), loginRequiredStrictly, adminRequired, async(req, res) => {
 
     const { configManager, slackIntegrationService } = crowi;
     const currentBotType = configManager.getConfig('slackbot:currentBotType');
@@ -334,25 +334,26 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put botType setting.
    */
-  // eslint-disable-next-line max-len
-  router.put('/bot-type', accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]), loginRequiredStrictly, adminRequired, addActivity, validator.botType, apiV3FormValidator, async(req, res) => {
-    const { currentBotType } = req.body;
+  router.put('/bot-type',
+    accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION], { acceptLegacy: true }),
+    loginRequiredStrictly, adminRequired, addActivity, validator.botType, apiV3FormValidator, async(req, res) => {
+      const { currentBotType } = req.body;
 
-    if (currentBotType == null) {
-      return res.apiv3Err(new ErrorV3('The param \'currentBotType\' must be specified.', 'update-CustomBotSetting-failed'), 400);
-    }
+      if (currentBotType == null) {
+        return res.apiv3Err(new ErrorV3('The param \'currentBotType\' must be specified.', 'update-CustomBotSetting-failed'), 400);
+      }
 
-    try {
-      await handleBotTypeChanging(req, res, currentBotType);
+      try {
+        await handleBotTypeChanging(req, res, currentBotType);
 
-      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE });
-    }
-    catch (error) {
-      const msg = 'Error occured in updating Custom bot setting';
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
-    }
-  });
+        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE });
+      }
+      catch (error) {
+        const msg = 'Error occured in updating Custom bot setting';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
+      }
+    });
 
   /**
    * @swagger
@@ -372,7 +373,8 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to delete botType setting.
    */
-  router.delete('/bot-type', accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]), loginRequiredStrictly, adminRequired, addActivity, apiV3FormValidator,
+  router.delete('/bot-type',
+    accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION], { acceptLegacy: true }), loginRequiredStrictly, adminRequired, addActivity, apiV3FormValidator,
     async(req, res) => {
       try {
         await handleBotTypeChanging(req, res, null);
@@ -466,34 +468,35 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put CustomBotWithoutProxy permissions.
    */
-  // eslint-disable-next-line max-len
-  router.put('/without-proxy/update-permissions', accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]), loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithoutProxy, async(req, res) => {
-    const currentBotType = crowi.configManager.getConfig('slackbot:currentBotType');
-    if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
-      const msg = 'Not CustomBotWithoutProxy';
-      return res.apiv3Err(new ErrorV3(msg, 'not-customBotWithoutProxy'), 400);
-    }
+  router.put('/without-proxy/update-permissions',
+    accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]),
+    loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithoutProxy, async(req, res) => {
+      const currentBotType = crowi.configManager.getConfig('slackbot:currentBotType');
+      if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
+        const msg = 'Not CustomBotWithoutProxy';
+        return res.apiv3Err(new ErrorV3(msg, 'not-customBotWithoutProxy'), 400);
+      }
 
-    // TODO: look here 78978
-    const { commandPermission, eventActionsPermission } = req.body;
-    const params = {
-      'slackbot:withoutProxy:commandPermission': commandPermission,
-      'slackbot:withoutProxy:eventActionsPermission': eventActionsPermission,
-    };
-    try {
-      await updateSlackBotSettings(params);
-      crowi.slackIntegrationService.publishUpdatedMessage();
+      // TODO: look here 78978
+      const { commandPermission, eventActionsPermission } = req.body;
+      const params = {
+        'slackbot:withoutProxy:commandPermission': commandPermission,
+        'slackbot:withoutProxy:eventActionsPermission': eventActionsPermission,
+      };
+      try {
+        await updateSlackBotSettings(params);
+        crowi.slackIntegrationService.publishUpdatedMessage();
 
-      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE });
+        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE });
 
-      return res.apiv3();
-    }
-    catch (error) {
-      const msg = 'Error occured in updating command permission settigns';
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
-    }
-  });
+        return res.apiv3();
+      }
+      catch (error) {
+        const msg = 'Error occured in updating command permission settigns';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
+      }
+    });
 
 
   /**
@@ -689,39 +692,40 @@ module.exports = (crowi) => {
    *          200:
    *            description: Succeeded to make it primary
    */
-  // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/make-primary', accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]), loginRequiredStrictly, adminRequired, addActivity, validator.makePrimary, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/make-primary',
+    accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]),
+    loginRequiredStrictly, adminRequired, addActivity, validator.makePrimary, apiV3FormValidator, async(req, res) => {
 
-    const { id } = req.params;
+      const { id } = req.params;
 
-    try {
-      await SlackAppIntegration.bulkWrite([
+      try {
+        await SlackAppIntegration.bulkWrite([
         // unset isPrimary for others
-        {
-          updateMany: {
-            filter: { _id: { $ne: id } },
-            update: { $unset: { isPrimary: '' } },
+          {
+            updateMany: {
+              filter: { _id: { $ne: id } },
+              update: { $unset: { isPrimary: '' } },
+            },
           },
-        },
-        // set primary
-        {
-          updateOne: {
-            filter: { _id: id },
-            update: { isPrimary: true },
+          // set primary
+          {
+            updateOne: {
+              filter: { _id: id },
+              update: { isPrimary: true },
+            },
           },
-        },
-      ]);
+        ]);
 
-      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY });
+        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY });
 
-      return res.apiv3();
-    }
-    catch (error) {
-      const msg = 'Error occurred during making SlackAppIntegration primary';
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'making-primary-failed'), 500);
-    }
-  });
+        return res.apiv3();
+      }
+      catch (error) {
+        const msg = 'Error occurred during making SlackAppIntegration primary';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'making-primary-failed'), 500);
+      }
+    });
 
   /**
    * @swagger
@@ -748,25 +752,26 @@ module.exports = (crowi) => {
    *                schema:
    *                  type: object
    */
-  // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/regenerate-tokens', accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]), loginRequiredStrictly, adminRequired, addActivity, validator.regenerateTokens, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/regenerate-tokens',
+    accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]),
+    loginRequiredStrictly, adminRequired, addActivity, validator.regenerateTokens, apiV3FormValidator, async(req, res) => {
 
-    const { id } = req.params;
+      const { id } = req.params;
 
-    try {
-      const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
-      const slackAppTokens = await SlackAppIntegration.findByIdAndUpdate(id, { tokenGtoP, tokenPtoG });
+      try {
+        const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
+        const slackAppTokens = await SlackAppIntegration.findByIdAndUpdate(id, { tokenGtoP, tokenPtoG });
 
-      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE });
+        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE });
 
-      return res.apiv3(slackAppTokens, 200);
-    }
-    catch (error) {
-      const msg = 'Error occurred during regenerating slack app tokens';
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'regenerating-slackAppTokens-failed'), 500);
-    }
-  });
+        return res.apiv3(slackAppTokens, 200);
+      }
+      catch (error) {
+        const msg = 'Error occurred during regenerating slack app tokens';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'regenerating-slackAppTokens-failed'), 500);
+      }
+    });
 
   /**
    * @swagger
@@ -805,51 +810,52 @@ module.exports = (crowi) => {
    *                schema:
    *                  type: object
    */
-  // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/permissions', accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]), loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithProxy, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/permissions',
+    accessTokenParser([SCOPE.WRITE.ADMIN.SLACK_INTEGRATION]),
+    loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithProxy, apiV3FormValidator, async(req, res) => {
     // TODO: look here 78975
-    const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands, permissionsForSlackEventActions } = req.body;
-    const { id } = req.params;
+      const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands, permissionsForSlackEventActions } = req.body;
+      const { id } = req.params;
 
-    const updatePermissionsForBroadcastUseCommands = new Map(Object.entries(permissionsForBroadcastUseCommands));
-    const updatePermissionsForSingleUseCommands = new Map(Object.entries(permissionsForSingleUseCommands));
-    const newPermissionsForSlackEventActions = new Map(Object.entries(permissionsForSlackEventActions));
+      const updatePermissionsForBroadcastUseCommands = new Map(Object.entries(permissionsForBroadcastUseCommands));
+      const updatePermissionsForSingleUseCommands = new Map(Object.entries(permissionsForSingleUseCommands));
+      const newPermissionsForSlackEventActions = new Map(Object.entries(permissionsForSlackEventActions));
 
 
-    try {
-      const slackAppIntegration = await SlackAppIntegration.findByIdAndUpdate(
-        id,
-        {
-          permissionsForBroadcastUseCommands: updatePermissionsForBroadcastUseCommands,
-          permissionsForSingleUseCommands: updatePermissionsForSingleUseCommands,
-          permissionsForSlackEventActions: newPermissionsForSlackEventActions,
-        },
-        { new: true },
-      );
-
-      const proxyUri = crowi.slackIntegrationService.proxyUriForCurrentType;
-      if (proxyUri != null) {
-        await requestToProxyServer(
-          slackAppIntegration.tokenGtoP,
-          'put',
-          '/g2s/supported-commands',
+      try {
+        const slackAppIntegration = await SlackAppIntegration.findByIdAndUpdate(
+          id,
           {
-            permissionsForBroadcastUseCommands: slackAppIntegration.permissionsForBroadcastUseCommands,
-            permissionsForSingleUseCommands: slackAppIntegration.permissionsForSingleUseCommands,
+            permissionsForBroadcastUseCommands: updatePermissionsForBroadcastUseCommands,
+            permissionsForSingleUseCommands: updatePermissionsForSingleUseCommands,
+            permissionsForSlackEventActions: newPermissionsForSlackEventActions,
           },
+          { new: true },
         );
-      }
 
-      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_PERMISSION_UPDATE });
+        const proxyUri = crowi.slackIntegrationService.proxyUriForCurrentType;
+        if (proxyUri != null) {
+          await requestToProxyServer(
+            slackAppIntegration.tokenGtoP,
+            'put',
+            '/g2s/supported-commands',
+            {
+              permissionsForBroadcastUseCommands: slackAppIntegration.permissionsForBroadcastUseCommands,
+              permissionsForSingleUseCommands: slackAppIntegration.permissionsForSingleUseCommands,
+            },
+          );
+        }
 
-      return res.apiv3({});
-    }
-    catch (error) {
-      const msg = `Error occured in updating settings. Cause: ${error.message}`;
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-permissions-failed'), 500);
-    }
-  });
+        activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_PERMISSION_UPDATE });
+
+        return res.apiv3({});
+      }
+      catch (error) {
+        const msg = `Error occured in updating settings. Cause: ${error.message}`;
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'update-permissions-failed'), 500);
+      }
+    });
 
   /**
    * @swagger

+ 1 - 1
apps/app/src/server/routes/apiv3/user/get-related-groups.ts

@@ -21,7 +21,7 @@ export const getRelatedGroupsHandlerFactory: GetRelatedGroupsHandlerFactory = (c
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   return [
-    accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO], { acceptLegacy: true }), loginRequiredStrictly,
     async(req: Req, res: ApiV3Response) => {
       try {
         const relatedGroups = await crowi.pageGrantService?.getUserRelatedGroups(req.user);

+ 114 - 112
apps/app/src/server/routes/apiv3/users.js

@@ -287,89 +287,90 @@ module.exports = (crowi) => {
    *                      $ref: '#/components/schemas/PaginateResult'
    */
 
-  router.get('/', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequired, validator.statusList, apiV3FormValidator, async(req, res) => {
-
-    const page = parseInt(req.query.page) || 1;
+  router.get('/',
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO], { acceptLegacy: true }), loginRequired, validator.statusList, apiV3FormValidator, async(req, res) => {
+
+      const page = parseInt(req.query.page) || 1;
+
+      // status
+      const forceIncludeAttributes = Array.isArray(req.query.forceIncludeAttributes)
+        ? req.query.forceIncludeAttributes
+        : [];
+      const selectedStatusList = Array.isArray(req.query.selectedStatusList)
+        ? req.query.selectedStatusList
+        : ['active'];
+
+      const statusNoList = (selectedStatusList.includes('all')) ? Object.values(statusNo) : selectedStatusList.map(element => statusNo[element]);
+
+      // Search from input
+      const searchText = req.query.searchText || '';
+      const searchWord = new RegExp(escapeStringRegexp(searchText));
+      // Sort
+      const { sort, sortOrder } = req.query;
+      const sortOutput = {
+        [sort]: (sortOrder === 'desc') ? -1 : 1,
+      };
+
+      //  For more information about the external specification of the User API, see here (https://dev.growi.org/5fd7466a31d89500488248e3)
+
+      const orConditions = [
+        { name: { $in: searchWord } },
+        { username: { $in: searchWord } },
+      ];
+
+      const query = {
+        $and: [
+          { status: { $in: statusNoList } },
+          {
+            $or: orConditions,
+          },
+        ],
+      };
 
-    // status
-    const forceIncludeAttributes = Array.isArray(req.query.forceIncludeAttributes)
-      ? req.query.forceIncludeAttributes
-      : [];
-    const selectedStatusList = Array.isArray(req.query.selectedStatusList)
-      ? req.query.selectedStatusList
-      : ['active'];
-
-    const statusNoList = (selectedStatusList.includes('all')) ? Object.values(statusNo) : selectedStatusList.map(element => statusNo[element]);
-
-    // Search from input
-    const searchText = req.query.searchText || '';
-    const searchWord = new RegExp(escapeStringRegexp(searchText));
-    // Sort
-    const { sort, sortOrder } = req.query;
-    const sortOutput = {
-      [sort]: (sortOrder === 'desc') ? -1 : 1,
-    };
-
-    //  For more information about the external specification of the User API, see here (https://dev.growi.org/5fd7466a31d89500488248e3)
-
-    const orConditions = [
-      { name: { $in: searchWord } },
-      { username: { $in: searchWord } },
-    ];
-
-    const query = {
-      $and: [
-        { status: { $in: statusNoList } },
-        {
-          $or: orConditions,
-        },
-      ],
-    };
+      try {
+        if (req.user != null) {
+          orConditions.push(
+            {
+              $and: [
+                { isEmailPublished: true },
+                { email: { $in: searchWord } },
+              ],
+            },
+          );
+        }
+        if (forceIncludeAttributes.includes('email')) {
+          orConditions.push({ email: { $in: searchWord } });
+        }
 
-    try {
-      if (req.user != null) {
-        orConditions.push(
+        const paginateResult = await User.paginate(
+          query,
           {
-            $and: [
-              { isEmailPublished: true },
-              { email: { $in: searchWord } },
-            ],
+            sort: sortOutput,
+            page,
+            limit: PAGE_ITEMS,
           },
         );
-      }
-      if (forceIncludeAttributes.includes('email')) {
-        orConditions.push({ email: { $in: searchWord } });
-      }
 
-      const paginateResult = await User.paginate(
-        query,
-        {
-          sort: sortOutput,
-          page,
-          limit: PAGE_ITEMS,
-        },
-      );
+        paginateResult.docs = paginateResult.docs.map((doc) => {
 
-      paginateResult.docs = paginateResult.docs.map((doc) => {
+          // return email only when specified by query
+          const { email } = doc;
+          const user = serializeUserSecurely(doc);
+          if (forceIncludeAttributes.includes('email')) {
+            user.email = email;
+          }
 
-        // return email only when specified by query
-        const { email } = doc;
-        const user = serializeUserSecurely(doc);
-        if (forceIncludeAttributes.includes('email')) {
-          user.email = email;
-        }
-
-        return user;
-      });
+          return user;
+        });
 
-      return res.apiv3({ paginateResult });
-    }
-    catch (err) {
-      const msg = 'Error occurred in fetching user group list';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'user-group-list-fetch-failed'), 500);
-    }
-  });
+        return res.apiv3({ paginateResult });
+      }
+      catch (err) {
+        const msg = 'Error occurred in fetching user group list';
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'user-group-list-fetch-failed'), 500);
+      }
+    });
 
   /**
    * @swagger
@@ -397,7 +398,7 @@ module.exports = (crowi) => {
    *                    paginateResult:
    *                      $ref: '#/components/schemas/PaginateResult'
    */
-  router.get('/:id/recent', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired,
+  router.get('/:id/recent', accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired,
     validator.recentCreatedByUser, apiV3FormValidator, async(req, res) => {
       const { id } = req.params;
 
@@ -1249,7 +1250,7 @@ module.exports = (crowi) => {
    *            500:
    *              $ref: '#/components/responses/500'
    */
-  router.get('/list', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequired, async(req, res) => {
+  router.get('/list', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO], { acceptLegacy: true }), loginRequired, async(req, res) => {
     const userIds = req.query.userIds ?? null;
 
     let userFetcher;
@@ -1353,48 +1354,49 @@ module.exports = (crowi) => {
     *                        items:
     *                          type: string
     */
-  router.get('/usernames', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequired, validator.usernames, apiV3FormValidator, async(req, res) => {
-    const q = req.query.q;
-    const offset = +req.query.offset || 0;
-    const limit = +req.query.limit || 10;
+  router.get('/usernames',
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO], { acceptLegacy: true }), loginRequired, validator.usernames, apiV3FormValidator, async(req, res) => {
+      const q = req.query.q;
+      const offset = +req.query.offset || 0;
+      const limit = +req.query.limit || 10;
 
-    try {
-      const options = JSON.parse(req.query.options || '{}');
-      const data = {};
+      try {
+        const options = JSON.parse(req.query.options || '{}');
+        const data = {};
 
-      if (options.isIncludeActiveUser == null || options.isIncludeActiveUser) {
-        const activeUserData = await User.findUserByUsernameRegexWithTotalCount(q, [User.STATUS_ACTIVE], { offset, limit });
-        const activeUsernames = activeUserData.users.map(user => user.username);
-        Object.assign(data, { activeUser: { usernames: activeUsernames, totalCount: activeUserData.totalCount } });
-      }
+        if (options.isIncludeActiveUser == null || options.isIncludeActiveUser) {
+          const activeUserData = await User.findUserByUsernameRegexWithTotalCount(q, [User.STATUS_ACTIVE], { offset, limit });
+          const activeUsernames = activeUserData.users.map(user => user.username);
+          Object.assign(data, { activeUser: { usernames: activeUsernames, totalCount: activeUserData.totalCount } });
+        }
 
-      if (options.isIncludeInactiveUser) {
-        const inactiveUserStates = [User.STATUS_REGISTERED, User.STATUS_SUSPENDED, User.STATUS_INVITED];
-        const inactiveUserData = await User.findUserByUsernameRegexWithTotalCount(q, inactiveUserStates, { offset, limit });
-        const inactiveUsernames = inactiveUserData.users.map(user => user.username);
-        Object.assign(data, { inactiveUser: { usernames: inactiveUsernames, totalCount: inactiveUserData.totalCount } });
-      }
+        if (options.isIncludeInactiveUser) {
+          const inactiveUserStates = [User.STATUS_REGISTERED, User.STATUS_SUSPENDED, User.STATUS_INVITED];
+          const inactiveUserData = await User.findUserByUsernameRegexWithTotalCount(q, inactiveUserStates, { offset, limit });
+          const inactiveUsernames = inactiveUserData.users.map(user => user.username);
+          Object.assign(data, { inactiveUser: { usernames: inactiveUsernames, totalCount: inactiveUserData.totalCount } });
+        }
 
-      if (options.isIncludeActivitySnapshotUser && req.user.admin) {
-        const activitySnapshotUserData = await Activity.findSnapshotUsernamesByUsernameRegexWithTotalCount(q, { offset, limit });
-        Object.assign(data, { activitySnapshotUser: activitySnapshotUserData });
-      }
+        if (options.isIncludeActivitySnapshotUser && req.user.admin) {
+          const activitySnapshotUserData = await Activity.findSnapshotUsernamesByUsernameRegexWithTotalCount(q, { offset, limit });
+          Object.assign(data, { activitySnapshotUser: activitySnapshotUserData });
+        }
 
-      // eslint-disable-next-line max-len
-      const canIncludeMixedUsernames = (options.isIncludeMixedUsernames && req.user.admin) || (options.isIncludeMixedUsernames && !options.isIncludeActivitySnapshotUser);
-      if (canIncludeMixedUsernames) {
-        const allUsernames = [...data.activeUser?.usernames || [], ...data.inactiveUser?.usernames || [], ...data?.activitySnapshotUser?.usernames || []];
-        const distinctUsernames = Array.from(new Set(allUsernames));
-        Object.assign(data, { mixedUsernames: distinctUsernames });
-      }
+        // eslint-disable-next-line max-len
+        const canIncludeMixedUsernames = (options.isIncludeMixedUsernames && req.user.admin) || (options.isIncludeMixedUsernames && !options.isIncludeActivitySnapshotUser);
+        if (canIncludeMixedUsernames) {
+          const allUsernames = [...data.activeUser?.usernames || [], ...data.inactiveUser?.usernames || [], ...data?.activitySnapshotUser?.usernames || []];
+          const distinctUsernames = Array.from(new Set(allUsernames));
+          Object.assign(data, { mixedUsernames: distinctUsernames });
+        }
 
-      return res.apiv3(data);
-    }
-    catch (err) {
-      logger.error('Failed to get usernames', err);
-      return res.apiv3Err(err);
-    }
-  });
+        return res.apiv3(data);
+      }
+      catch (err) {
+        logger.error('Failed to get usernames', err);
+        return res.apiv3Err(err);
+      }
+    });
 
   return router;
 };

+ 14 - 14
apps/app/src/server/routes/index.js

@@ -123,26 +123,26 @@ module.exports = function(crowi, app) {
 
   const apiV1Router = express.Router();
 
-  apiV1Router.get('/search'                        , accessTokenParser([SCOPE.READ.FEATURES.PAGE]) , loginRequired , search.api.search);
+  apiV1Router.get('/search'              , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }) , loginRequired , search.api.search);
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
-  apiV1Router.get('/pages.updatePost'    , accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, page.api.getUpdatePost);
-  apiV1Router.get('/pages.getPageTag'    , accessTokenParser([SCOPE.READ.FEATURES.PAGE]) , loginRequired , page.api.getPageTag);
+  apiV1Router.get('/pages.updatePost'    , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, page.api.getUpdatePost);
+  apiV1Router.get('/pages.getPageTag'    , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }) , loginRequired , page.api.getPageTag);
   // allow posting to guests because the client doesn't know whether the user logged in
   apiV1Router.post('/pages.remove'       , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.validator.remove, apiV1FormValidator, page.api.remove); // (Avoid from API Token)
   apiV1Router.post('/pages.revertRemove' , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.validator.revertRemove, apiV1FormValidator, page.api.revertRemove); // (Avoid from API Token)
   apiV1Router.post('/pages.unlink'       , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.api.unlink); // (Avoid from API Token)
-  apiV1Router.get('/tags.list'           , accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, tag.api.list);
-  apiV1Router.get('/tags.search'         , accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, tag.api.search);
-  apiV1Router.post('/tags.update'        , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, excludeReadOnlyUser, addActivity, tag.api.update);
-  apiV1Router.get('/comments.get'        , accessTokenParser([SCOPE.READ.FEATURES.PAGE]) , loginRequired , comment.api.get);
-  apiV1Router.post('/comments.add'       , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.add);
-  apiV1Router.post('/comments.update'    , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.update);
-  apiV1Router.post('/comments.remove'    , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.remove);
-
-  apiV1Router.post('/attachments.uploadProfileImage'   , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT]), uploads.single('file'), accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, uploads.single('file'), autoReap, attachmentApi.uploadProfileImage);
-  apiV1Router.post('/attachments.remove'               , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT]), loginRequiredStrictly , excludeReadOnlyUser, addActivity ,attachmentApi.remove);
-  apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT]), loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.removeProfileImage);
+  apiV1Router.get('/tags.list'           , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, tag.api.list);
+  apiV1Router.get('/tags.search'         , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, tag.api.search);
+  apiV1Router.post('/tags.update'        , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, excludeReadOnlyUser, addActivity, tag.api.update);
+  apiV1Router.get('/comments.get'        , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }) , loginRequired , comment.api.get);
+  apiV1Router.post('/comments.add'       , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.add);
+  apiV1Router.post('/comments.update'    , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.update);
+  apiV1Router.post('/comments.remove'    , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.remove);
+
+  apiV1Router.post('/attachments.uploadProfileImage'   , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], { acceptLegacy: true }), uploads.single('file'), accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, uploads.single('file'), autoReap, attachmentApi.uploadProfileImage);
+  apiV1Router.post('/attachments.remove'               , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], { acceptLegacy: true }), loginRequiredStrictly , excludeReadOnlyUser, addActivity ,attachmentApi.remove);
+  apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], { acceptLegacy: true }), loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.removeProfileImage);
 
   // API v1
   app.use('/_api', unavailableWhenMaintenanceModeForApi, apiV1Router);

+ 2 - 0
packages/remark-attachment-refs/src/server/routes/refs.ts

@@ -83,6 +83,7 @@ export const routesFactory = (crowi): any => {
   /**
    * return an Attachment model
    */
+  // TODO: https://redmine.weseek.co.jp/issues/166911
   router.get('/ref', accessTokenParser(), loginRequired, async(req: RequestWithUser, res) => {
     const user = req.user;
     const { pagePath, fileNameOrId } = req.query;
@@ -138,6 +139,7 @@ export const routesFactory = (crowi): any => {
   /**
    * return a list of Attachment
    */
+  // TODO: https://redmine.weseek.co.jp/issues/166911
   router.get('/refs', accessTokenParser(), loginRequired, async(req: RequestWithUser, res) => {
     const user = req.user;
     const { prefix, pagePath } = req.query;

+ 1 - 1
packages/remark-lsx/src/server/index.ts

@@ -59,7 +59,7 @@ const middleware = (crowi: any, app: any): void => {
 
   app.get(
     '/_api/lsx',
-    accessTokenParser(),
+    accessTokenParser(), // TODO: https://redmine.weseek.co.jp/issues/166911
     loginRequired,
     lsxValidator,
     paramValidator,