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

feat: update access token parser scopes for various API routes

reiji-h 1 год назад
Родитель
Сommit
a63a56bf9e
42 измененных файлов с 183 добавлено и 180 удалено
  1. 2 2
      apps/app/src/client/components/Me/AccessTokenForm.tsx
  2. 1 1
      apps/app/src/features/openai/server/routes/ai-assistant.ts
  3. 1 1
      apps/app/src/features/openai/server/routes/ai-assistants.ts
  4. 1 1
      apps/app/src/features/openai/server/routes/delete-ai-assistant.ts
  5. 1 1
      apps/app/src/features/openai/server/routes/delete-thread.ts
  6. 1 1
      apps/app/src/features/openai/server/routes/get-messages.ts
  7. 1 1
      apps/app/src/features/openai/server/routes/get-threads.ts
  8. 1 1
      apps/app/src/features/openai/server/routes/message.ts
  9. 1 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. 6 6
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  13. 3 3
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  14. 18 15
      apps/app/src/interfaces/scope.ts
  15. 8 8
      apps/app/src/server/middlewares/access-token-parser/access-token.integ.ts
  16. 4 4
      apps/app/src/server/routes/apiv3/attachment.js
  17. 6 6
      apps/app/src/server/routes/apiv3/bookmark-folder.ts
  18. 3 3
      apps/app/src/server/routes/apiv3/bookmarks.js
  19. 4 4
      apps/app/src/server/routes/apiv3/in-app-notification.ts
  20. 4 4
      apps/app/src/server/routes/apiv3/page-listing.ts
  21. 1 1
      apps/app/src/server/routes/apiv3/page/check-page-existence.ts
  22. 1 1
      apps/app/src/server/routes/apiv3/page/create-page.ts
  23. 1 1
      apps/app/src/server/routes/apiv3/page/get-page-paths-with-descendant-count.ts
  24. 1 1
      apps/app/src/server/routes/apiv3/page/get-yjs-data.ts
  25. 11 11
      apps/app/src/server/routes/apiv3/page/index.ts
  26. 1 1
      apps/app/src/server/routes/apiv3/page/publish-page.ts
  27. 1 1
      apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts
  28. 1 1
      apps/app/src/server/routes/apiv3/page/unpublish-page.ts
  29. 1 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  30. 11 11
      apps/app/src/server/routes/apiv3/pages/index.js
  31. 1 1
      apps/app/src/server/routes/apiv3/personal-setting/delete-access-token.ts
  32. 1 1
      apps/app/src/server/routes/apiv3/personal-setting/delete-all-access-tokens.ts
  33. 1 1
      apps/app/src/server/routes/apiv3/personal-setting/get-access-tokens.ts
  34. 18 18
      apps/app/src/server/routes/apiv3/personal-setting/index.js
  35. 2 2
      apps/app/src/server/routes/apiv3/revisions.js
  36. 5 5
      apps/app/src/server/routes/apiv3/share-links.js
  37. 1 1
      apps/app/src/server/routes/apiv3/user/get-related-groups.ts
  38. 6 6
      apps/app/src/server/routes/apiv3/users.js
  39. 1 1
      apps/app/src/server/routes/attachment/get-brand-logo.ts
  40. 25 25
      apps/app/src/server/routes/index.js
  41. 22 22
      apps/app/src/server/util/scope-util.spec.ts
  42. 2 2
      apps/app/src/server/util/scope-utils.ts

+ 2 - 2
apps/app/src/client/components/Me/AccessTokenForm.tsx

@@ -117,7 +117,7 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
               <input
                 type="checkbox"
                 id="scope-read-user"
-                value={SCOPE.READ.USER.ALL}
+                value={SCOPE.READ.USER_SETTINGS.ALL}
                 {...register('scopes')}
               />
               <label htmlFor="scope-read-user" className="ms-2">Read User</label>
@@ -126,7 +126,7 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
               <input
                 type="checkbox"
                 id="scope-write-user"
-                value={SCOPE.WRITE.USER.ALL}
+                value={SCOPE.WRITE.USER_SETTINGS.ALL}
                 {...register('scopes')}
               />
               <label htmlFor="scope-write-user" className="ms-2">Write User</label>

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

@@ -29,7 +29,7 @@ export const createAiAssistantFactory: CreateAssistantFactory = (crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
   return [
-    accessTokenParser([SCOPE.WRITE.BASE.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, upsertAiAssistantValidator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), 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.BASE.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService,
+    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT]), 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.BASE.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), 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.BASE.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { aiAssistantId, threadRelationId } = req.params;
       const { user } = req;

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

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

+ 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.BASE.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.READ.FEATURES.AI_ASSISTANT]), 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.ts

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

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

@@ -39,7 +39,7 @@ export const setDefaultAiAssistantFactory: setDefaultAiAssistantFactory = (crowi
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.BASE.AI_ASSISTANT]), loginRequiredStrictly, adminRequired, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), 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

@@ -35,7 +35,7 @@ export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.BASE.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), 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.BASE.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
+    accessTokenParser([SCOPE.WRITE.FEATURES.AI_ASSISTANT]), loginRequiredStrictly, certifyAiService, validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { id } = req.params;
       const { user } = req;

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

@@ -62,7 +62,7 @@ module.exports = (crowi: Crowi): Router => {
     return 404;
   };
 
-  router.get('/orders', accessTokenParser([SCOPE.READ.BASE.QUESTIONNAIRE]), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  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));
 
@@ -77,12 +77,12 @@ module.exports = (crowi: Crowi): Router => {
     }
   });
 
-  router.get('/is-enabled', accessTokenParser([SCOPE.READ.BASE.QUESTIONNAIRE]), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  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.post('/proactive/answer', accessTokenParser([SCOPE.WRITE.BASE.QUESTIONNAIRE]), loginRequired,
+  router.post('/proactive/answer', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
     validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const sendQuestionnaireAnswer = async() => {
         const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
@@ -132,7 +132,7 @@ module.exports = (crowi: Crowi): Router => {
       }
     });
 
-  router.put('/answer', accessTokenParser([SCOPE.WRITE.BASE.QUESTIONNAIRE]), loginRequired,
+  router.put('/answer', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
     validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
         const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
@@ -180,7 +180,7 @@ module.exports = (crowi: Crowi): Router => {
       }
     });
 
-  router.put('/skip', accessTokenParser([SCOPE.WRITE.BASE.QUESTIONNAIRE]), loginRequired,
+  router.put('/skip', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
     validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const errors = validationResult(req);
       if (!errors.isEmpty()) {
@@ -197,7 +197,7 @@ module.exports = (crowi: Crowi): Router => {
       }
     });
 
-  router.put('/deny', accessTokenParser([SCOPE.WRITE.BASE.QUESTIONNAIRE]), loginRequired,
+  router.put('/deny', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequired,
     validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
       const errors = validationResult(req);
       if (!errors.isEmpty()) {

+ 3 - 3
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -38,7 +38,7 @@ let presetTemplateSummaries: TemplateSummary[];
 module.exports = (crowi: Crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
-  router.get('/', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
+  router.get('/', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
     const { includeInvalidTemplates } = req.query;
 
     // scan preset templates
@@ -74,7 +74,7 @@ module.exports = (crowi: Crowi) => {
     });
   });
 
-  router.get('/preset-templates/:templateId/:locale', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequiredStrictly,
+  router.get('/preset-templates/:templateId/:locale', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly,
     validator.get, apiV3FormValidator,
     async(req, res: ApiV3Response) => {
       const {
@@ -92,7 +92,7 @@ module.exports = (crowi: Crowi) => {
       }
     });
 
-  router.get('/plugin-templates/:organizationId/:reposId/:templateId/:locale', accessTokenParser([SCOPE.READ.BASE.PAGE]),
+  router.get('/plugin-templates/:organizationId/:reposId/:templateId/:locale', accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
     loginRequiredStrictly, validator.get, apiV3FormValidator, async(
         req, res: ApiV3Response,
     ) => {

+ 18 - 15
apps/app/src/interfaces/scope.ts

@@ -1,5 +1,8 @@
-// If you want to add a new scope, you only need to add a new key to the ORIGINAL_SCOPE object.
-export const ORIGINAL_SCOPE_ADMIN = {
+// If you want to add a new scope, you only need to add a new key to the SCOPE_SEED object.
+
+// admin と user で分けたいとき = /me で管理者とユーザーで扱えるスコープが違う
+
+const SCOPE_SEED_ADMIN = {
   admin: {
     top: {},
     app: {},
@@ -21,8 +24,8 @@ export const ORIGINAL_SCOPE_ADMIN = {
   },
 } as const;
 
-export const ORIGINAL_SCOPE_USER = {
-  user: {
+const SCOPE_SEED_USER = {
+  user_settings: {
     info: {},
     external_account: {},
     password: {},
@@ -33,7 +36,7 @@ export const ORIGINAL_SCOPE_USER = {
     in_app_notification: {},
     other: {},
   },
-  base: {
+  features: {
     ai_assistant: {},
     page: {},
     share_link: {},
@@ -43,9 +46,9 @@ export const ORIGINAL_SCOPE_USER = {
   },
 } as const;
 
-export const ORIGINAL_SCOPE = {
-  ...ORIGINAL_SCOPE_ADMIN,
-  ...ORIGINAL_SCOPE_USER,
+export const SCOPE_SEED = {
+  ...SCOPE_SEED_ADMIN,
+  ...SCOPE_SEED_USER,
 } as const;
 
 export const ACTION = {
@@ -56,12 +59,12 @@ export const ACTION = {
 type ACTION_TYPE = typeof ACTION[keyof typeof ACTION];
 export const ALL_SIGN = '*';
 
-export const ORIGINAL_SCOPE_WITH_ACTION = Object.values(ACTION).reduce(
+export const SCOPE_SEED_WITH_ACTION = Object.values(ACTION).reduce(
   (acc, action) => {
-    acc[action] = ORIGINAL_SCOPE;
+    acc[action] = SCOPE_SEED;
     return acc;
   },
-  {} as Record<ACTION_TYPE, typeof ORIGINAL_SCOPE>,
+  {} as Record<ACTION_TYPE, typeof SCOPE_SEED>,
 );
 
 type FlattenObject<T> = {
@@ -77,7 +80,7 @@ type AddAllToScope<S extends string> =
     ? `${X}:${typeof ALL_SIGN}` | `${X}:${AddAllToScope<Y>}` | S
     : S;
 
-type ScopeOnly = FlattenObject<typeof ORIGINAL_SCOPE_WITH_ACTION>;
+type ScopeOnly = FlattenObject<typeof SCOPE_SEED_WITH_ACTION>;
 type ScopeWithAll = AddAllToScope<ScopeOnly>;
 export type Scope = ScopeOnly | ScopeWithAll;
 
@@ -93,8 +96,8 @@ type ScopeConstantNode<T> = {
 };
 
 type ScopeConstantType = {
-  [A in keyof typeof ORIGINAL_SCOPE_WITH_ACTION as Uppercase<string & A>]:
-    ScopeConstantNode<typeof ORIGINAL_SCOPE> & { ALL: Scope }
+  [A in keyof typeof SCOPE_SEED_WITH_ACTION as Uppercase<string & A>]:
+    ScopeConstantNode<typeof SCOPE_SEED> & { ALL: Scope }
 };
 
 const buildScopeConstants = (): ScopeConstantType => {
@@ -122,7 +125,7 @@ const buildScopeConstants = (): ScopeConstantType => {
       }
     });
   };
-  processObject(ORIGINAL_SCOPE_WITH_ACTION, [], result);
+  processObject(SCOPE_SEED_WITH_ACTION, [], result);
 
   return result as ScopeConstantType;
 };

+ 8 - 8
apps/app/src/server/middlewares/access-token-parser/access-token.integ.ts

@@ -102,12 +102,12 @@ describe('access-token-parser middleware for access token with scopes', () => {
     const { token } = await AccessToken.generateToken(
       targetUser._id,
       new Date(Date.now() + 1000 * 60 * 60 * 24),
-      [SCOPE.READ.USER.INFO],
+      [SCOPE.READ.USER_SETTINGS.INFO],
     );
 
     // act
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.READ.USER.INFO])(reqMock, resMock, nextMock);
+    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO])(reqMock, resMock, nextMock);
 
     // assert
     expect(reqMock.user).toBeDefined();
@@ -139,12 +139,12 @@ describe('access-token-parser middleware for access token with scopes', () => {
     const { token } = await AccessToken.generateToken(
       targetUser._id,
       new Date(Date.now() + 1000 * 60 * 60 * 24),
-      [SCOPE.READ.USER.INFO],
+      [SCOPE.READ.USER_SETTINGS.INFO],
     );
 
     // act - try to access with write:user:info scope
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.WRITE.USER.INFO])(reqMock, resMock, nextMock);
+    await parserForAccessToken([SCOPE.WRITE.USER_SETTINGS.INFO])(reqMock, resMock, nextMock);
 
     // // assert
     expect(reqMock.user).toBeUndefined();
@@ -174,12 +174,12 @@ describe('access-token-parser middleware for access token with scopes', () => {
     const { token } = await AccessToken.generateToken(
       targetUser._id,
       new Date(Date.now() + 1000 * 60 * 60 * 24),
-      [SCOPE.WRITE.USER.INFO],
+      [SCOPE.WRITE.USER_SETTINGS.INFO],
     );
 
     // act - try to access with read:user:info scope
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.READ.USER.INFO])(reqMock, resMock, nextMock);
+    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO])(reqMock, resMock, nextMock);
 
     // assert
     expect(reqMock.user).toBeDefined();
@@ -208,12 +208,12 @@ describe('access-token-parser middleware for access token with scopes', () => {
     const { token } = await AccessToken.generateToken(
       targetUser._id,
       new Date(Date.now() + 1000 * 60 * 60 * 24),
-      [SCOPE.READ.USER.ALL],
+      [SCOPE.READ.USER_SETTINGS.ALL],
     );
 
     // act - try to access with read:user:info scope
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.READ.USER.INFO, SCOPE.READ.USER.API.ACCESS_TOKEN])(reqMock, resMock, nextMock);
+    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO, SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN])(reqMock, resMock, nextMock);
 
     // assert
     expect(reqMock.user).toBeDefined();

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

@@ -199,7 +199,7 @@ module.exports = (crowi) => {
    *                  type: object
    *                  $ref: '#/components/schemas/AttachmentPaginateResult'
    */
-  router.get('/list', accessTokenParser([SCOPE.READ.BASE.ATTACHMENT]), loginRequired, validator.retrieveAttachments, apiV3FormValidator, async(req, res) => {
+  router.get('/list', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), loginRequired, validator.retrieveAttachments, apiV3FormValidator, async(req, res) => {
 
     const limit = req.query.limit || await crowi.configManager.getConfig('customize:showPageLimitationS') || 10;
     const pageNumber = req.query.pageNumber || 1;
@@ -273,7 +273,7 @@ module.exports = (crowi) => {
    *          500:
    *            $ref: '#/components/responses/500'
    */
-  router.get('/limit', accessTokenParser([SCOPE.READ.BASE.ATTACHMENT]), loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator,
+  router.get('/limit', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator,
     async(req, res) => {
       const { fileUploadService } = crowi;
       const fileSize = Number(req.query.fileSize);
@@ -341,7 +341,7 @@ module.exports = (crowi) => {
    *          500:
    *            $ref: '#/components/responses/500'
    */
-  router.post('/', uploads.single('file'), autoReap, accessTokenParser([SCOPE.WRITE.BASE.ATTACHMENT]), loginRequiredStrictly, excludeReadOnlyUser,
+  router.post('/', uploads.single('file'), autoReap, accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT]), loginRequiredStrictly, excludeReadOnlyUser,
     validator.retrieveAddAttachment, apiV3FormValidator, addActivity,
     async(req, res) => {
 
@@ -405,7 +405,7 @@ module.exports = (crowi) => {
    *            schema:
    *              type: string
    */
-  router.get('/:id', accessTokenParser([SCOPE.READ.BASE.ATTACHMENT]), certifySharedPageAttachmentMiddleware, loginRequired,
+  router.get('/:id', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), certifySharedPageAttachmentMiddleware, loginRequired,
     validator.retrieveAttachment, apiV3FormValidator,
     async(req, res) => {
       try {

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

@@ -157,7 +157,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.post('/', accessTokenParser([SCOPE.WRITE.BASE.BOOKMARK]), loginRequiredStrictly, validator.bookmarkFolder, apiV3FormValidator, async(req, res) => {
+  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 = {
@@ -209,7 +209,7 @@ module.exports = (crowi) => {
    *                        type: object
    *                        $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.get('/list/:userId', accessTokenParser([SCOPE.READ.BASE.BOOKMARK]), loginRequiredStrictly, async(req, res) => {
+  router.get('/list/:userId', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK]), loginRequiredStrictly, async(req, res) => {
     const { userId } = req.params;
 
     const getBookmarkFolders = async(
@@ -297,7 +297,7 @@ module.exports = (crowi) => {
    *                      description: Number of deleted folders
    *                      example: 1
    */
-  router.delete('/:id', accessTokenParser([SCOPE.WRITE.BASE.BOOKMARK]), loginRequiredStrictly, async(req, res) => {
+  router.delete('/:id', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, async(req, res) => {
     const { id } = req.params;
     try {
       const result = await BookmarkFolder.deleteFolderAndChildren(id);
@@ -353,7 +353,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.put('/', accessTokenParser([SCOPE.WRITE.BASE.BOOKMARK]), loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
+  router.put('/', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
     const {
       bookmarkFolderId, name, parent, childFolder,
     } = req.body;
@@ -402,7 +402,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.post('/add-bookmark-to-folder', accessTokenParser([SCOPE.WRITE.BASE.BOOKMARK]), loginRequiredStrictly, validator.bookmarkPage, apiV3FormValidator,
+  router.post('/add-bookmark-to-folder', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, validator.bookmarkPage, apiV3FormValidator,
     async(req, res) => {
       const userId = req.user?._id;
       const { pageId, folderId } = req.body;
@@ -452,7 +452,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/BookmarkFolder'
    */
-  router.put('/update-bookmark', accessTokenParser([SCOPE.WRITE.BASE.BOOKMARK]), loginRequiredStrictly, validator.bookmark, async(req, res) => {
+  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 {

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

@@ -111,7 +111,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/BookmarkInfo'
    */
-  router.get('/info', accessTokenParser([SCOPE.READ.BASE.BOOKMARK]), loginRequired, validator.bookmarkInfo, apiV3FormValidator, async(req, res) => {
+  router.get('/info', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK]), loginRequired, validator.bookmarkInfo, apiV3FormValidator, async(req, res) => {
     const { user } = req;
     const { pageId } = req.query;
 
@@ -193,7 +193,7 @@ module.exports = (crowi) => {
     param('userId').isMongoId().withMessage('userId is required'),
   ];
 
-  router.get('/:userId', accessTokenParser([SCOPE.READ.BASE.BOOKMARK]), loginRequired, validator.userBookmarkList, apiV3FormValidator, async(req, res) => {
+  router.get('/:userId', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK]), loginRequired, validator.userBookmarkList, apiV3FormValidator, async(req, res) => {
     const { userId } = req.params;
 
     if (userId == null) {
@@ -247,7 +247,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Bookmark'
    */
-  router.put('/', accessTokenParser([SCOPE.WRITE.BASE.BOOKMARK]), loginRequiredStrictly, addActivity, validator.bookmarks, apiV3FormValidator,
+  router.put('/', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK]), loginRequiredStrictly, addActivity, validator.bookmarks, apiV3FormValidator,
     async(req, res) => {
       const { pageId, bool } = req.body;
       const userId = req.user?._id;

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

@@ -25,7 +25,7 @@ module.exports = (crowi) => {
 
   const activityEvent = crowi.event('activity');
 
-  router.get('/list', accessTokenParser([SCOPE.READ.USER.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
+  router.get('/list', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const user = req.user!;
@@ -80,7 +80,7 @@ module.exports = (crowi) => {
     return res.apiv3(serializedPaginationResult);
   });
 
-  router.get('/status', accessTokenParser([SCOPE.READ.USER.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
+  router.get('/status', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const user = req.user!;
@@ -94,7 +94,7 @@ module.exports = (crowi) => {
     }
   });
 
-  router.post('/open', accessTokenParser([SCOPE.WRITE.USER.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
+  router.post('/open', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const user = req.user!;
@@ -111,7 +111,7 @@ module.exports = (crowi) => {
     }
   });
 
-  router.put('/all-statuses-open', accessTokenParser([SCOPE.WRITE.USER.IN_APP_NOTIFICATION]), loginRequiredStrictly, addActivity,
+  router.put('/all-statuses-open', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly, addActivity,
     async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

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

@@ -67,7 +67,7 @@ const routerFactory = (crowi: Crowi): Router => {
   const router = express.Router();
 
 
-  router.get('/root', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.get('/root', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const Page = mongoose.model<IPage, PageModel>('Page');
 
     let rootPage;
@@ -82,7 +82,7 @@ const routerFactory = (crowi: Crowi): Router => {
   });
 
   // eslint-disable-next-line max-len
-  router.get('/ancestors-children', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+  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;
@@ -101,7 +101,7 @@ 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.BASE.PAGE]), loginRequired, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.get('/children', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const { id, path } = req.query;
 
     const pageService = crowi.pageService;
@@ -122,7 +122,7 @@ const routerFactory = (crowi: Crowi): Router => {
   });
 
   // eslint-disable-next-line max-len
-  router.get('/info', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, validator.pageIdsOrPathRequired, validator.infoParams, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  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;

+ 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.BASE.PAGE]), loginRequired,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), 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.BASE.PAGE]), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), 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.BASE.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]), 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.BASE.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;

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

@@ -212,7 +212,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Page'
    */
-  router.get('/', accessTokenParser([SCOPE.READ.BASE.PAGE]), certifySharedPage, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
+  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,
@@ -443,7 +443,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Page'
    */
-  router.put('/likes', accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly, addActivity, validator.likes, apiV3FormValidator, async(req, res) => {
+  router.put('/likes', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, addActivity, validator.likes, apiV3FormValidator, async(req, res) => {
     const { pageId, bool: isLiked } = req.body;
 
     let page;
@@ -513,7 +513,7 @@ module.exports = (crowi) => {
    *          500:
    *            description: Internal server error.
    */
-  router.get('/info', accessTokenParser([SCOPE.READ.BASE.PAGE]), certifySharedPage, loginRequired, validator.info, apiV3FormValidator, async(req, res) => {
+  router.get('/info', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), certifySharedPage, loginRequired, validator.info, apiV3FormValidator, async(req, res) => {
     const { user, isSharedPage } = req;
     const { pageId } = req.query;
 
@@ -563,7 +563,7 @@ module.exports = (crowi) => {
    *          500:
    *            description: Internal server error.
    */
-  router.get('/grant-data', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequiredStrictly, validator.getGrantData, apiV3FormValidator, async(req, res) => {
+  router.get('/grant-data', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly, validator.getGrantData, apiV3FormValidator, async(req, res) => {
     const { pageId } = req.query;
 
     const Page = mongoose.model<IPage, PageModel>('Page');
@@ -665,7 +665,7 @@ module.exports = (crowi) => {
    *         500:
    *           description: Internal server error.
    */
-  router.get('/non-user-related-groups-granted', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequiredStrictly,
+  router.get('/non-user-related-groups-granted', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly,
     validator.nonUserRelatedGroupsGranted, apiV3FormValidator,
     async(req, res: ApiV3Response) => {
       const { user } = req;
@@ -736,7 +736,7 @@ module.exports = (crowi) => {
    *         500:
    *           description: Internal server error.
    */
-  router.get('/applicable-grant', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequiredStrictly, validator.applicableGrant, apiV3FormValidator,
+  router.get('/applicable-grant', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly, validator.applicableGrant, apiV3FormValidator,
     async(req, res) => {
       const { pageId } = req.query;
 
@@ -797,7 +797,7 @@ module.exports = (crowi) => {
    *               schema:
    *                 $ref: '#/components/schemas/Page'
    */
-  router.put('/:pageId/grant', accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly, excludeReadOnlyUser,
+  router.put('/:pageId/grant', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, excludeReadOnlyUser,
     validator.updateGrant, apiV3FormValidator,
     async(req, res) => {
       const { pageId } = req.params;
@@ -839,7 +839,7 @@ module.exports = (crowi) => {
   *          200:
   *            description: Return page's markdown
   */
-  router.get('/export/:pageId', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequiredStrictly, validator.export, async(req, res) => {
+  router.get('/export/:pageId', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequiredStrictly, validator.export, async(req, res) => {
     const pageId: string = req.params.pageId;
     const { format, revisionId = null } = req.query;
     let revision;
@@ -963,7 +963,7 @@ module.exports = (crowi) => {
    *          500:
    *            description: Internal server error.
    */
-  router.get('/exist-paths', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, validator.exist, apiV3FormValidator, async(req, res) => {
+  router.get('/exist-paths', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, validator.exist, apiV3FormValidator, async(req, res) => {
     const { fromPath, toPath } = req.query;
 
     try {
@@ -1017,7 +1017,7 @@ module.exports = (crowi) => {
    *          500:
    *            description: Internal server error.
    */
-  router.put('/subscribe', accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly, addActivity,
+  router.put('/subscribe', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, addActivity,
     validator.subscribe, apiV3FormValidator,
     async(req, res) => {
       const { pageId, status } = req.body;
@@ -1079,7 +1079,7 @@ module.exports = (crowi) => {
    *                   page:
    *                     $ref: '#/components/schemas/Page'
    */
-  router.put('/:pageId/content-width', accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly, excludeReadOnlyUser,
+  router.put('/:pageId/content-width', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), 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.BASE.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), 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.BASE.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), 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.BASE.PAGE]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), 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.BASE.PAGE]), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
     validator, apiV3FormValidator,
     async(req: UpdatePageRequest, res: ApiV3Response) => {
       const {

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

@@ -152,7 +152,7 @@ module.exports = (crowi) => {
    *          200:
    *            description: Return pages recently updated
    */
-  router.get('/recent', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, validator.recent, apiV3FormValidator, async(req, res) => {
+  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
@@ -273,7 +273,7 @@ module.exports = (crowi) => {
    */
   router.put(
     '/rename',
-    accessTokenParser([SCOPE.WRITE.BASE.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     validator.renamePage,
@@ -381,7 +381,7 @@ module.exports = (crowi) => {
     */
   router.post(
     '/resume-rename',
-    accessTokenParser([SCOPE.WRITE.BASE.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
     loginRequiredStrictly,
     validator.resumeRenamePage,
     apiV3FormValidator,
@@ -437,7 +437,7 @@ module.exports = (crowi) => {
    */
   router.delete(
     '/empty-trash',
-    accessTokenParser([SCOPE.WRITE.BASE.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
     loginRequired,
     excludeReadOnlyUser,
     addActivity,
@@ -551,7 +551,7 @@ module.exports = (crowi) => {
     */
   router.get(
     '/list',
-    accessTokenParser([SCOPE.READ.BASE.PAGE]),
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
     loginRequired,
     validator.displayList,
     apiV3FormValidator,
@@ -634,7 +634,7 @@ module.exports = (crowi) => {
    */
   router.post(
     '/duplicate',
-    accessTokenParser([SCOPE.WRITE.BASE.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     addActivity,
@@ -739,7 +739,7 @@ module.exports = (crowi) => {
    */
   router.get(
     '/subordinated-list',
-    accessTokenParser([SCOPE.READ.BASE.PAGE]),
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
     loginRequired,
     async(req, res) => {
       const { path } = req.query;
@@ -803,7 +803,7 @@ module.exports = (crowi) => {
     */
   router.post(
     '/delete',
-    accessTokenParser([SCOPE.WRITE.BASE.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     validator.deletePages,
@@ -892,7 +892,7 @@ module.exports = (crowi) => {
   // eslint-disable-next-line max-len
   router.post(
     '/convert-pages-by-path',
-    accessTokenParser([SCOPE.WRITE.BASE.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
     loginRequiredStrictly,
     excludeReadOnlyUser,
     adminRequired,
@@ -953,7 +953,7 @@ module.exports = (crowi) => {
   // eslint-disable-next-line max-len
   router.post(
     '/legacy-pages-migration',
-    accessTokenParser([SCOPE.WRITE.BASE.PAGE]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
     loginRequired,
     excludeReadOnlyUser,
     validator.legacyPagesMigration,
@@ -1010,7 +1010,7 @@ module.exports = (crowi) => {
    */
   router.get(
     '/v5-migration-status',
-    accessTokenParser([SCOPE.READ.BASE.PAGE]),
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE]),
     loginRequired,
     async(req, res) => {
       try {

+ 1 - 1
apps/app/src/server/routes/apiv3/personal-setting/delete-access-token.ts

@@ -38,7 +38,7 @@ export const deleteAccessTokenHandlersFactory: DeleteAccessTokenHandlersFactory
   const activityEvent = crowi.event('activity');
 
   return [
-    accessTokenParser([SCOPE.WRITE.USER.API.ACCESS_TOKEN]),
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN]),
     loginRequiredStrictly,
     addActivity,
     validator,

+ 1 - 1
apps/app/src/server/routes/apiv3/personal-setting/delete-all-access-tokens.ts

@@ -27,7 +27,7 @@ export const deleteAllAccessTokensHandlersFactory: DeleteAllAccessTokensHandlers
   const activityEvent = crowi.event('activity');
 
   return [
-    accessTokenParser([SCOPE.WRITE.USER.API.ACCESS_TOKEN]),
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN]),
     loginRequiredStrictly,
     addActivity,
     async(req: DeleteAllAccessTokensRequest, res: ApiV3Response) => {

+ 1 - 1
apps/app/src/server/routes/apiv3/personal-setting/get-access-tokens.ts

@@ -25,7 +25,7 @@ export const getAccessTokenHandlerFactory: GetAccessTokenHandlerFactory = (crowi
   const addActivity = generateAddActivityMiddleware();
 
   return [
-    accessTokenParser([SCOPE.READ.USER.API.ACCESS_TOKEN]),
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN]),
     loginRequiredStrictly,
     addActivity,
     async(req: GetAccessTokenRequest, res: ApiV3Response) => {

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

@@ -152,7 +152,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: personal params
    */
-  router.get('/', accessTokenParser([SCOPE.READ.USER.INFO]), loginRequiredStrictly, async(req, res) => {
+  router.get('/', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequiredStrictly, async(req, res) => {
     const { username } = req.user;
     try {
       const user = await User.findUserByUsername(username);
@@ -190,7 +190,7 @@ module.exports = (crowi) => {
    *                    isPasswordSet:
    *                      type: boolean
    */
-  router.get('/is-password-set', accessTokenParser([SCOPE.READ.USER.PASSWORD]), loginRequiredStrictly, async(req, res) => {
+  router.get('/is-password-set', accessTokenParser([SCOPE.READ.USER_SETTINGS.PASSWORD]), loginRequiredStrictly, async(req, res) => {
     const { username } = req.user;
 
     try {
@@ -232,7 +232,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: personal params
    */
-  router.put('/', accessTokenParser([SCOPE.WRITE.USER.INFO]), loginRequiredStrictly, addActivity, validator.personal, apiV3FormValidator, async(req, res) => {
+  router.put('/', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.INFO]), loginRequiredStrictly, addActivity, validator.personal, apiV3FormValidator, async(req, res) => {
 
     try {
       const user = await User.findOne({ _id: req.user.id });
@@ -283,7 +283,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: user data
    */
-  router.put('/image-type', accessTokenParser([SCOPE.WRITE.USER.INFO]), loginRequiredStrictly, addActivity,
+  router.put('/image-type', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.INFO]), loginRequiredStrictly, addActivity,
     validator.imageType, apiV3FormValidator,
     async(req, res) => {
       const { isGravatarEnabled } = req.body;
@@ -322,7 +322,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: array of external accounts
    */
-  router.get('/external-accounts', accessTokenParser([SCOPE.READ.USER.EXTERNAL_ACCOUNT]), loginRequiredStrictly, async(req, res) => {
+  router.get('/external-accounts', accessTokenParser([SCOPE.READ.USER_SETTINGS.EXTERNAL_ACCOUNT]), loginRequiredStrictly, async(req, res) => {
     const userData = req.user;
 
     try {
@@ -362,7 +362,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: user data updated
    */
-  router.put('/password', accessTokenParser([SCOPE.WRITE.USER.PASSWORD]), loginRequiredStrictly, addActivity, validator.password, apiV3FormValidator,
+  router.put('/password', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.PASSWORD]), loginRequiredStrictly, addActivity, validator.password, apiV3FormValidator,
     async(req, res) => {
       const { body, user } = req;
       const { oldPassword, newPassword } = body;
@@ -405,7 +405,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: user data
    */
-  router.put('/api-token', accessTokenParser([SCOPE.WRITE.USER.API.API_TOKEN]), loginRequiredStrictly, addActivity, async(req, res) => {
+  router.put('/api-token', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.API.API_TOKEN]), loginRequiredStrictly, addActivity, async(req, res) => {
     const { user } = req;
 
     try {
@@ -442,7 +442,7 @@ module.exports = (crowi) => {
    *                   type: objet
    *                   description: array of access tokens
    */
-  router.get('/access-token', accessTokenParser([SCOPE.READ.USER.API.ACCESS_TOKEN]), getAccessTokenHandlerFactory(crowi));
+  router.get('/access-token', accessTokenParser([SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN]), getAccessTokenHandlerFactory(crowi));
 
   /**
    * @swagger
@@ -475,7 +475,7 @@ module.exports = (crowi) => {
    *                     type: string[]
    *                     description: scope of access token
    */
-  router.post('/access-token', accessTokenParser([SCOPE.WRITE.USER.API.ACCESS_TOKEN]), generateAccessTokenHandlerFactory(crowi));
+  router.post('/access-token', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN]), generateAccessTokenHandlerFactory(crowi));
 
   /**
    * @swagger
@@ -490,7 +490,7 @@ module.exports = (crowi) => {
    *         description: succeded to delete access token
    *
    */
-  router.delete('/access-token', accessTokenParser([SCOPE.WRITE.USER.API.ACCESS_TOKEN]), deleteAccessTokenHandlersFactory(crowi));
+  router.delete('/access-token', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN]), deleteAccessTokenHandlersFactory(crowi));
 
   /**
    * @swagger
@@ -504,7 +504,7 @@ module.exports = (crowi) => {
    *         200:
    *           description: succeded to delete all access tokens
    */
-  router.delete('/access-token/all', accessTokenParser([SCOPE.WRITE.USER.API.ACCESS_TOKEN]), deleteAllAccessTokensHandlersFactory(crowi));
+  router.delete('/access-token/all', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN]), deleteAllAccessTokensHandlersFactory(crowi));
 
   /**
    * @swagger
@@ -532,7 +532,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: Ldap account associate to me
    */
-  router.put('/associate-ldap', accessTokenParser([SCOPE.WRITE.USER.EXTERNAL_ACCOUNT]), loginRequiredStrictly, addActivity,
+  router.put('/associate-ldap', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.EXTERNAL_ACCOUNT]), loginRequiredStrictly, addActivity,
     validator.associateLdap, apiV3FormValidator,
     async(req, res) => {
       const { passportService } = crowi;
@@ -587,7 +587,7 @@ module.exports = (crowi) => {
    *                      description: Ldap account disassociate to me
    */
   // eslint-disable-next-line max-len
-  router.put('/disassociate-ldap', accessTokenParser([SCOPE.WRITE.USER.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;
@@ -632,7 +632,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: editor settings
    */
-  router.put('/editor-settings', accessTokenParser([SCOPE.WRITE.USER.OTHER]), loginRequiredStrictly, addActivity, validator.editorSettings, apiV3FormValidator,
+  router.put('/editor-settings', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.OTHER]), loginRequiredStrictly, addActivity, validator.editorSettings, apiV3FormValidator,
     async(req, res) => {
       const query = { userId: req.user.id };
       const { body } = req;
@@ -683,7 +683,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: editor settings
    */
-  router.get('/editor-settings', accessTokenParser([SCOPE.READ.USER.OTHER]), loginRequiredStrictly, async(req, res) => {
+  router.get('/editor-settings', accessTokenParser([SCOPE.READ.USER_SETTINGS.OTHER]), loginRequiredStrictly, async(req, res) => {
     try {
       const query = { userId: req.user.id };
       const editorSettings = await EditorSettings.findOne(query) ?? new EditorSettings();
@@ -716,7 +716,7 @@ module.exports = (crowi) => {
    *                      description: in-app-notification-settings
    */
   // eslint-disable-next-line max-len
-  router.put('/in-app-notification-settings', accessTokenParser([SCOPE.WRITE.USER.IN_APP_NOTIFICATION]), loginRequiredStrictly, addActivity, validator.inAppNotificationSettings, apiV3FormValidator, async(req, res) => {
+  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;
 
@@ -759,7 +759,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: InAppNotificationSettings
    */
-  router.get('/in-app-notification-settings', accessTokenParser([SCOPE.READ.USER.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req, res) => {
+  router.get('/in-app-notification-settings', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION]), loginRequiredStrictly, async(req, res) => {
     const query = { userId: req.user.id };
     try {
       const response = await InAppNotificationSettings.findOne(query);
@@ -772,7 +772,7 @@ module.exports = (crowi) => {
   });
 
   // eslint-disable-next-line max-len
-  router.put('/questionnaire-settings', accessTokenParser([SCOPE.WRITE.BASE.QUESTIONNAIRE]), loginRequiredStrictly, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
+  router.put('/questionnaire-settings', accessTokenParser([SCOPE.WRITE.FEATURES.QUESTIONNAIRE]), loginRequiredStrictly, validator.questionnaireSettings, apiV3FormValidator, async(req, res) => {
     const { isQuestionnaireEnabled } = req.body;
     const { user } = req;
     try {

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

@@ -135,7 +135,7 @@ module.exports = (crowi) => {
    *                    type: number
    *                    description: offset of the revisions
    */
-  router.get('/list', certifySharedPage, accessTokenParser(SCOPE.READ.BASE.PAGE), loginRequired, validator.retrieveRevisions, apiV3FormValidator,
+  router.get('/list', certifySharedPage, accessTokenParser(SCOPE.READ.FEATURES.PAGE), 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 +235,7 @@ module.exports = (crowi) => {
    *                    revision:
    *                      $ref: '#/components/schemas/Revision'
    */
-  router.get('/:id', certifySharedPage, accessTokenParser(SCOPE.READ.BASE.PAGE), loginRequired, validator.retrieveRevisionById, apiV3FormValidator,
+  router.get('/:id', certifySharedPage, accessTokenParser(SCOPE.READ.FEATURES.PAGE), loginRequired, validator.retrieveRevisionById, apiV3FormValidator,
     async(req, res) => {
       const revisionId = req.params.id;
       const pageId = req.query.pageId;

+ 5 - 5
apps/app/src/server/routes/apiv3/share-links.js

@@ -139,7 +139,7 @@ module.exports = (crowi) => {
    *                        $ref: '#/components/schemas/ShareLink'
    */
   router.get('/',
-    accessTokenParser([SCOPE.READ.BASE.SHARE_LINK]),
+    accessTokenParser([SCOPE.READ.FEATURES.SHARE_LINK]),
     loginRequired,
     linkSharingRequired,
     validator.getShareLinks,
@@ -211,7 +211,7 @@ module.exports = (crowi) => {
    *                 $ref: '#/components/schemas/ShareLinkSimple'
    */
   router.post('/',
-    accessTokenParser([SCOPE.WRITE.BASE.SHARE_LINK]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]),
     loginRequired,
     excludeReadOnlyUser,
     linkSharingRequired,
@@ -275,7 +275,7 @@ module.exports = (crowi) => {
   *                 $ref: '#/components/schemas/ShareLinkSimple'
   */
   router.delete('/',
-    accessTokenParser([SCOPE.WRITE.BASE.SHARE_LINK]),
+    accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]),
     loginRequired,
     excludeReadOnlyUser,
     addActivity,
@@ -326,7 +326,7 @@ module.exports = (crowi) => {
   *                      type: integer
   *                      description: The number of share links deleted
   */
-  router.delete('/all', accessTokenParser([SCOPE.WRITE.BASE.SHARE_LINK]), loginRequired, adminRequired, addActivity, async(req, res) => {
+  router.delete('/all', accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]), loginRequired, adminRequired, addActivity, async(req, res) => {
 
     try {
       const deletedShareLink = await ShareLink.deleteMany({});
@@ -367,7 +367,7 @@ module.exports = (crowi) => {
   *          200:
   *            description: Succeeded to delete one share link
   */
-  router.delete('/:id', accessTokenParser([SCOPE.WRITE.BASE.SHARE_LINK]), loginRequired, excludeReadOnlyUser, addActivity,
+  router.delete('/:id', accessTokenParser([SCOPE.WRITE.FEATURES.SHARE_LINK]), loginRequired, excludeReadOnlyUser, addActivity,
     validator.deleteShareLink, apiV3FormValidator,
     async(req, res) => {
       const { id } = req.params;

+ 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.INFO]), loginRequiredStrictly,
+    accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequiredStrictly,
     async(req: Req, res: ApiV3Response) => {
       try {
         const relatedGroups = await crowi.pageGrantService?.getUserRelatedGroups(req.user);

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

@@ -246,7 +246,7 @@ module.exports = (crowi) => {
    *                      $ref: '#/components/schemas/PaginateResult'
    */
 
-  router.get('/', accessTokenParser([SCOPE.READ.USER.INFO]), loginRequired, validator.statusList, apiV3FormValidator, async(req, res) => {
+  router.get('/', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequired, validator.statusList, apiV3FormValidator, async(req, res) => {
 
     const page = parseInt(req.query.page) || 1;
     // status
@@ -351,7 +351,7 @@ module.exports = (crowi) => {
    *                    paginateResult:
    *                      $ref: '#/components/schemas/PaginateResult'
    */
-  router.get('/:id/recent', accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, validator.recentCreatedByUser, apiV3FormValidator, async(req, res) => {
+  router.get('/:id/recent', accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, validator.recentCreatedByUser, apiV3FormValidator, async(req, res) => {
     const { id } = req.params;
 
     let user;
@@ -858,7 +858,7 @@ module.exports = (crowi) => {
    *                    paginateResult:
    *                      $ref: '#/components/schemas/PaginateResult'
    */
-  router.get('/external-accounts/', accessTokenParser([SCOPE.READ.USER.EXTERNAL_ACCOUNT]), loginRequiredStrictly, adminRequired, async(req, res) => {
+  router.get('/external-accounts/', accessTokenParser([SCOPE.READ.USER_SETTINGS.EXTERNAL_ACCOUNT]), loginRequiredStrictly, adminRequired, async(req, res) => {
     const page = parseInt(req.query.page) || 1;
     try {
       const paginateResult = await ExternalAccount.findAllWithPagination({ page });
@@ -899,7 +899,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: A result of `ExtenralAccount.findByIdAndRemove`
    */
-  router.delete('/external-accounts/:id/remove', accessTokenParser([SCOPE.WRITE.USER.EXTERNAL_ACCOUNT]),
+  router.delete('/external-accounts/:id/remove', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.EXTERNAL_ACCOUNT]),
     loginRequiredStrictly, adminRequired, apiV3FormValidator,
     async(req, res) => {
       const { id } = req.params;
@@ -1145,7 +1145,7 @@ module.exports = (crowi) => {
    *            500:
    *              $ref: '#/components/responses/500'
    */
-  router.get('/list', accessTokenParser([SCOPE.READ.USER.INFO]), loginRequired, async(req, res) => {
+  router.get('/list', accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequired, async(req, res) => {
     const userIds = req.query.userIds ?? null;
 
     let userFetcher;
@@ -1174,7 +1174,7 @@ module.exports = (crowi) => {
     return res.apiv3(data);
   });
 
-  router.get('/usernames', accessTokenParser([SCOPE.READ.USER.INFO]), loginRequired, validator.usernames, apiV3FormValidator, async(req, res) => {
+  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;

+ 1 - 1
apps/app/src/server/routes/attachment/get-brand-logo.ts

@@ -27,7 +27,7 @@ export const getBrandLogoRouterFactory = (crowi: Crowi): Router => {
 
   const router = express.Router();
 
-  router.get('/brand-logo', certifyBrandLogo, accessTokenParser([SCOPE.READ.BASE.ATTACHMENT]), loginRequired, async(req: CrowiRequest, res: Response) => {
+  router.get('/brand-logo', certifyBrandLogo, accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), loginRequired, async(req: CrowiRequest, res: Response) => {
     const brandLogoAttachment = await Attachment.findOne({ attachmentType: AttachmentType.BRAND_LOGO });
 
     if (brandLogoAttachment == null) {

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

@@ -71,7 +71,7 @@ module.exports = function(crowi, app) {
 
   app.get('/_next/*'                  , next.delegateToNext);
 
-  app.get('/'                         , accessTokenParser([SCOPE.READ.BASE.PAGE]), applicationInstalled, unavailableWhenMaintenanceMode, loginRequired, autoReconnectToSearch, next.delegateToNext);
+  app.get('/'                         , accessTokenParser([SCOPE.READ.FEATURES.PAGE]), applicationInstalled, unavailableWhenMaintenanceMode, loginRequired, autoReconnectToSearch, next.delegateToNext);
 
   app.get('/login/error/:reason'      , applicationInstalled, next.delegateToNext);
   app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
@@ -122,39 +122,39 @@ module.exports = function(crowi, app) {
 
   const apiV1Router = express.Router();
 
-  apiV1Router.get('/search'                        , accessTokenParser([SCOPE.READ.BASE.PAGE]) , loginRequired , search.api.search);
+  apiV1Router.get('/search'                        , accessTokenParser([SCOPE.READ.FEATURES.PAGE]) , loginRequired , search.api.search);
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
-  apiV1Router.get('/pages.updatePost'    , accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, page.api.getUpdatePost);
-  apiV1Router.get('/pages.getPageTag'    , accessTokenParser([SCOPE.READ.BASE.PAGE]) , loginRequired , page.api.getPageTag);
+  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);
   // allow posting to guests because the client doesn't know whether the user logged in
-  apiV1Router.post('/pages.remove'       , accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.validator.remove, apiV1FormValidator, page.api.remove); // (Avoid from API Token)
-  apiV1Router.post('/pages.revertRemove' , accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.validator.revertRemove, apiV1FormValidator, page.api.revertRemove); // (Avoid from API Token)
-  apiV1Router.post('/pages.unlink'       , accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.api.unlink); // (Avoid from API Token)
-  apiV1Router.get('/tags.list'           , accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, tag.api.list);
-  apiV1Router.get('/tags.search'         , accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, tag.api.search);
-  apiV1Router.post('/tags.update'        , accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly, excludeReadOnlyUser, addActivity, tag.api.update);
-  apiV1Router.get('/comments.get'        , accessTokenParser([SCOPE.READ.BASE.PAGE]) , loginRequired , comment.api.get);
-  apiV1Router.post('/comments.add'       , accessTokenParser([SCOPE.WRITE.BASE.PAGE]), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.add);
-  apiV1Router.post('/comments.update'    , accessTokenParser([SCOPE.WRITE.BASE.PAGE]), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.update);
-  apiV1Router.post('/comments.remove'    , accessTokenParser([SCOPE.WRITE.BASE.PAGE]), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.remove);
-
-  apiV1Router.post('/attachments.uploadProfileImage'   , accessTokenParser([SCOPE.WRITE.BASE.ATTACHMENT]), uploads.single('file'), autoReap, loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.uploadProfileImage);
-  apiV1Router.post('/attachments.remove'               , accessTokenParser([SCOPE.WRITE.BASE.ATTACHMENT]), loginRequiredStrictly , excludeReadOnlyUser, addActivity ,attachmentApi.remove);
-  apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser([SCOPE.WRITE.BASE.ATTACHMENT]), loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.removeProfileImage);
+  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'), autoReap, loginRequiredStrictly , excludeReadOnlyUser, 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);
 
   // API v1
   app.use('/_api', unavailableWhenMaintenanceModeForApi, apiV1Router);
 
   app.use(unavailableWhenMaintenanceMode);
 
-  app.get('/me'                                   , accessTokenParser([SCOPE.READ.USER.INFO]), loginRequiredStrictly, next.delegateToNext);
-  app.get('/me/*'                                 , accessTokenParser([SCOPE.READ.USER.INFO]), loginRequiredStrictly, next.delegateToNext);
+  app.get('/me'                                   , accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequiredStrictly, next.delegateToNext);
+  app.get('/me/*'                                 , accessTokenParser([SCOPE.READ.USER_SETTINGS.INFO]), loginRequiredStrictly, next.delegateToNext);
 
-  app.use('/attachment', accessTokenParser([SCOPE.READ.BASE.ATTACHMENT]), attachment.getRouterFactory(crowi));
-  app.use('/download', accessTokenParser([SCOPE.READ.BASE.ATTACHMENT]), attachment.downloadRouterFactory(crowi));
+  app.use('/attachment', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), attachment.getRouterFactory(crowi));
+  app.use('/download', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), attachment.downloadRouterFactory(crowi));
 
-  app.get('/_search'                            , accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, next.delegateToNext);
+  app.get('/_search'                            , accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, next.delegateToNext);
 
   app.use('/forgot-password', express.Router()
     .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(crowi))
@@ -173,7 +173,7 @@ module.exports = function(crowi, app) {
 
   app.use('/ogp', express.Router().get('/:pageId([0-9a-z]{0,})', loginRequired, ogp.pageIdRequired, ogp.ogpValidator, ogp.renderOgp));
 
-  app.get('/*/$'                   , accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, next.delegateToNext);
-  app.get('/*'                     , accessTokenParser([SCOPE.READ.BASE.PAGE]), loginRequired, autoReconnectToSearch, next.delegateToNext);
+  app.get('/*/$'                   , accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, next.delegateToNext);
+  app.get('/*'                     , accessTokenParser([SCOPE.READ.FEATURES.PAGE]), loginRequired, autoReconnectToSearch, next.delegateToNext);
 
 };

+ 22 - 22
apps/app/src/server/util/scope-util.spec.ts

@@ -9,8 +9,8 @@ import {
 describe('scope-utils', () => {
   describe('isValidScope', () => {
     it('should return true for valid scopes', () => {
-      expect(isValidScope(SCOPE.READ.USER.API.API_TOKEN)).toBe(true);
-      expect(isValidScope(SCOPE.WRITE.USER.API.ACCESS_TOKEN)).toBe(true);
+      expect(isValidScope(SCOPE.READ.USER_SETTINGS.API.API_TOKEN)).toBe(true);
+      expect(isValidScope(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN)).toBe(true);
       expect(isValidScope(SCOPE.READ.ADMIN.APP)).toBe(true);
     });
 
@@ -23,26 +23,26 @@ describe('scope-utils', () => {
 
   describe('hasAllScope', () => {
     it('should return true for scopes ending with *', () => {
-      expect(hasAllScope(SCOPE.READ.USER.API.ALL)).toBe(true);
+      expect(hasAllScope(SCOPE.READ.USER_SETTINGS.API.ALL)).toBe(true);
       expect(hasAllScope(SCOPE.WRITE.ADMIN.ALL)).toBe(true);
     });
 
     it('should return false for specific scopes', () => {
-      expect(hasAllScope(SCOPE.READ.USER.API.API_TOKEN)).toBe(false);
-      expect(hasAllScope(SCOPE.WRITE.USER.API.ACCESS_TOKEN)).toBe(false);
+      expect(hasAllScope(SCOPE.READ.USER_SETTINGS.API.API_TOKEN)).toBe(false);
+      expect(hasAllScope(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN)).toBe(false);
     });
   });
 
   describe('extractAllScope', () => {
     it('should extract all specific scopes from ALL scope', () => {
-      const extracted = extractAllScope(SCOPE.READ.USER.API.ALL);
-      expect(extracted).toContain(SCOPE.READ.USER.API.API_TOKEN);
-      expect(extracted).toContain(SCOPE.READ.USER.API.ACCESS_TOKEN);
-      expect(extracted).not.toContain(SCOPE.READ.USER.API.ALL);
+      const extracted = extractAllScope(SCOPE.READ.USER_SETTINGS.API.ALL);
+      expect(extracted).toContain(SCOPE.READ.USER_SETTINGS.API.API_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN);
+      expect(extracted).not.toContain(SCOPE.READ.USER_SETTINGS.API.ALL);
     });
 
     it('should return array with single scope for specific scope', () => {
-      const scope = SCOPE.READ.USER.API.API_TOKEN;
+      const scope = SCOPE.READ.USER_SETTINGS.API.API_TOKEN;
       const extracted = extractAllScope(scope);
       expect(extracted).toEqual([scope]);
     });
@@ -54,32 +54,32 @@ describe('scope-utils', () => {
     });
 
     it('should extract all implied scopes including READ permission for WRITE scopes', () => {
-      const scopes = [SCOPE.WRITE.USER.API.ACCESS_TOKEN];
+      const scopes = [SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN];
       const extracted = extractScopes(scopes);
 
-      expect(extracted).toContain(SCOPE.WRITE.USER.API.ACCESS_TOKEN);
-      expect(extracted).toContain(SCOPE.READ.USER.API.ACCESS_TOKEN);
+      expect(extracted).toContain(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN);
     });
 
     it('should extract all specific scopes from ALL scope with implied permissions', () => {
-      const scopes = [SCOPE.WRITE.USER.API.ALL];
+      const scopes = [SCOPE.WRITE.USER_SETTINGS.API.ALL];
       const extracted = extractScopes(scopes);
 
       // Should include both WRITE and READ permissions for all specific scopes
-      expect(extracted).toContain(SCOPE.WRITE.USER.API.API_TOKEN);
-      expect(extracted).toContain(SCOPE.WRITE.USER.API.ACCESS_TOKEN);
-      expect(extracted).toContain(SCOPE.READ.USER.API.API_TOKEN);
-      expect(extracted).toContain(SCOPE.READ.USER.API.ACCESS_TOKEN);
+      expect(extracted).toContain(SCOPE.WRITE.USER_SETTINGS.API.API_TOKEN);
+      expect(extracted).toContain(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER_SETTINGS.API.API_TOKEN);
+      expect(extracted).toContain(SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN);
 
       // Should not include ALL scopes
-      expect(extracted).not.toContain(SCOPE.WRITE.USER.API.ALL);
-      expect(extracted).not.toContain(SCOPE.READ.USER.API.ALL);
+      expect(extracted).not.toContain(SCOPE.WRITE.USER_SETTINGS.API.ALL);
+      expect(extracted).not.toContain(SCOPE.READ.USER_SETTINGS.API.ALL);
     });
 
     it('should remove duplicate scopes', () => {
       const scopes = [
-        SCOPE.WRITE.USER.API.ACCESS_TOKEN,
-        SCOPE.READ.USER.API.ACCESS_TOKEN, // This is implied by WRITE
+        SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN,
+        SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN, // This is implied by WRITE
       ];
       const extracted = extractScopes(scopes);
 

+ 2 - 2
apps/app/src/server/util/scope-utils.ts

@@ -20,7 +20,7 @@ export const hasAllScope = (scope: Scope): scope is Scope => {
 
 /**
  * Returns all values of the scope object
- * For example, SCOPE.READ.USER.API.ALL returns ['read:user:api:access_token', 'read:user:api:api_token']
+ * For example, SCOPE.READ.USER_SETTINGS.API.ALL returns ['read:user:api:access_token', 'read:user:api:api_token']
  */
 const getAllScopeValuesFromObj = (scopeObj: any): Scope[] => {
   const result: Scope[] = [];
@@ -78,7 +78,7 @@ export const extractAllScope = (scope: Scope): Scope[] => {
 /**
  * Extracts scopes from a given array of scopes
  * And delete all scopes
- * For example, [SCOPE.WRITE.USER.API.ALL] === ['write:user:api:all']
+ * For example, [SCOPE.WRITE.USER_SETTINGS.API.ALL] === ['write:user:api:all']
  * returns ['read:user:api:access_token',
  *          'read:user:api:api_token'
  *          'write:user:api:access_token',