Jelajahi Sumber

configure biome for some app server services

Futa Arai 4 bulan lalu
induk
melakukan
b0bb415dc8

+ 32 - 36
apps/app/src/server/service/acl.integ.ts

@@ -3,22 +3,24 @@ import type { MockInstance } from 'vitest';
 import { aclService } from './acl';
 import { configManager } from './config-manager';
 
-
 describe('AclService', () => {
   test("has consts 'isLabeledStatement'", () => {
     expect(aclService.labels.SECURITY_RESTRICT_GUEST_MODE_DENY).toBe('Deny');
-    expect(aclService.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY).toBe('Readonly');
+    expect(aclService.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY).toBe(
+      'Readonly',
+    );
     expect(aclService.labels.SECURITY_REGISTRATION_MODE_OPEN).toBe('Open');
-    expect(aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED).toBe('Restricted');
+    expect(aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED).toBe(
+      'Restricted',
+    );
     expect(aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED).toBe('Closed');
   });
 });
 
 describe('AclService test', () => {
-
   const initialEnv = process.env;
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     await configManager.loadConfigs();
   });
 
@@ -27,8 +29,7 @@ describe('AclService test', () => {
   });
 
   describe('isAclEnabled()', () => {
-
-    test('to be false when FORCE_WIKI_MODE is undefined', async() => {
+    test('to be false when FORCE_WIKI_MODE is undefined', async () => {
       delete process.env.FORCE_WIKI_MODE;
 
       // reload
@@ -41,7 +42,7 @@ describe('AclService test', () => {
       expect(result).toBe(true);
     });
 
-    test('to be false when FORCE_WIKI_MODE is dummy string', async() => {
+    test('to be false when FORCE_WIKI_MODE is dummy string', async () => {
       process.env.FORCE_WIKI_MODE = 'dummy string';
 
       // reload
@@ -54,7 +55,7 @@ describe('AclService test', () => {
       expect(result).toBe(true);
     });
 
-    test('to be true when FORCE_WIKI_MODE=private', async() => {
+    test('to be true when FORCE_WIKI_MODE=private', async () => {
       process.env.FORCE_WIKI_MODE = 'private';
 
       // reload
@@ -67,7 +68,7 @@ describe('AclService test', () => {
       expect(result).toBe(true);
     });
 
-    test('to be false when FORCE_WIKI_MODE=public', async() => {
+    test('to be false when FORCE_WIKI_MODE=public', async () => {
       process.env.FORCE_WIKI_MODE = 'public';
 
       // reload
@@ -79,13 +80,10 @@ describe('AclService test', () => {
       expect(wikiMode).toBe('public');
       expect(result).toBe(false);
     });
-
   });
 
-
   describe('isWikiModeForced()', () => {
-
-    test('to be false when FORCE_WIKI_MODE is undefined', async() => {
+    test('to be false when FORCE_WIKI_MODE is undefined', async () => {
       delete process.env.FORCE_WIKI_MODE;
 
       // reload
@@ -98,7 +96,7 @@ describe('AclService test', () => {
       expect(result).toBe(false);
     });
 
-    test('to be false when FORCE_WIKI_MODE is dummy string', async() => {
+    test('to be false when FORCE_WIKI_MODE is dummy string', async () => {
       process.env.FORCE_WIKI_MODE = 'dummy string';
 
       // reload
@@ -111,7 +109,7 @@ describe('AclService test', () => {
       expect(result).toBe(false);
     });
 
-    test('to be true when FORCE_WIKI_MODE=private', async() => {
+    test('to be true when FORCE_WIKI_MODE=private', async () => {
       process.env.FORCE_WIKI_MODE = 'private';
 
       // reload
@@ -124,7 +122,7 @@ describe('AclService test', () => {
       expect(result).toBe(true);
     });
 
-    test('to be false when FORCE_WIKI_MODE=public', async() => {
+    test('to be false when FORCE_WIKI_MODE=public', async () => {
       process.env.FORCE_WIKI_MODE = 'public';
 
       // reload
@@ -136,19 +134,17 @@ describe('AclService test', () => {
       expect(wikiMode).toBe('public');
       expect(result).toBe(true);
     });
-
   });
 
-
   describe('isGuestAllowedToRead()', () => {
     let getConfigSpy: MockInstance<typeof configManager.getConfig>;
 
-    beforeEach(async() => {
+    beforeEach(async () => {
       // prepare spy for ConfigManager.getConfig
       getConfigSpy = vi.spyOn(configManager, 'getConfig');
     });
 
-    test('to be false when FORCE_WIKI_MODE=private', async() => {
+    test('to be false when FORCE_WIKI_MODE=private', async () => {
       process.env.FORCE_WIKI_MODE = 'private';
 
       // reload
@@ -158,11 +154,13 @@ describe('AclService test', () => {
 
       const wikiMode = configManager.getConfig('security:wikiMode');
       expect(wikiMode).toBe('private');
-      expect(getConfigSpy).not.toHaveBeenCalledWith('security:restrictGuestMode');
+      expect(getConfigSpy).not.toHaveBeenCalledWith(
+        'security:restrictGuestMode',
+      );
       expect(result).toBe(false);
     });
 
-    test('to be true when FORCE_WIKI_MODE=public', async() => {
+    test('to be true when FORCE_WIKI_MODE=public', async () => {
       process.env.FORCE_WIKI_MODE = 'public';
 
       // reload
@@ -172,22 +170,23 @@ describe('AclService test', () => {
 
       const wikiMode = configManager.getConfig('security:wikiMode');
       expect(wikiMode).toBe('public');
-      expect(getConfigSpy).not.toHaveBeenCalledWith('security:restrictGuestMode');
+      expect(getConfigSpy).not.toHaveBeenCalledWith(
+        'security:restrictGuestMode',
+      );
       expect(result).toBe(true);
     });
 
     /* eslint-disable indent */
     describe.each`
-      restrictGuestMode   | expected
-      ${undefined}        | ${false}
-      ${'Deny'}           | ${false}
-      ${'Readonly'}       | ${true}
-      ${'Open'}           | ${false}
-      ${'Restricted'}     | ${false}
-      ${'closed'}         | ${false}
+      restrictGuestMode | expected
+      ${undefined}      | ${false}
+      ${'Deny'}         | ${false}
+      ${'Readonly'}     | ${true}
+      ${'Open'}         | ${false}
+      ${'Restricted'}   | ${false}
+      ${'closed'}       | ${false}
     `('to be $expected', ({ restrictGuestMode, expected }) => {
-      test(`when FORCE_WIKI_MODE is undefined and 'security:restrictGuestMode' is '${restrictGuestMode}`, async() => {
-
+      test(`when FORCE_WIKI_MODE is undefined and 'security:restrictGuestMode' is '${restrictGuestMode}`, async () => {
         // reload
         await configManager.loadConfigs();
 
@@ -210,8 +209,5 @@ describe('AclService test', () => {
         expect(result).toBe(expected);
       });
     });
-
   });
-
-
 });

+ 5 - 7
apps/app/src/server/service/acl.ts

@@ -6,18 +6,17 @@ import { configManager } from './config-manager';
 const logger = loggerFactory('growi:service:AclService');
 
 export interface AclService {
-  get labels(): { [key: string]: string },
-  isAclEnabled(): boolean,
-  isWikiModeForced(): boolean,
-  isGuestAllowedToRead(): boolean,
-  getGuestModeValue(): string,
+  get labels(): { [key: string]: string };
+  isAclEnabled(): boolean;
+  isWikiModeForced(): boolean;
+  isGuestAllowedToRead(): boolean;
+  getGuestModeValue(): string;
 }
 
 /**
  * the service class of AclService
  */
 class AclServiceImpl implements AclService {
-
   get labels() {
     return {
       SECURITY_RESTRICT_GUEST_MODE_DENY: 'Deny',
@@ -73,7 +72,6 @@ class AclServiceImpl implements AclService {
       ? this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY
       : this.labels.SECURITY_RESTRICT_GUEST_MODE_DENY;
   }
-
 }
 
 export const aclService = new AclServiceImpl();

+ 93 - 57
apps/app/src/server/service/activity.ts

@@ -3,16 +3,18 @@ import mongoose from 'mongoose';
 
 import type { IActivity, SupportedActionType } from '~/interfaces/activity';
 import {
-  AllSupportedActions, ActionGroupSize,
-  AllEssentialActions, AllSmallGroupActions, AllMediumGroupActions, AllLargeGroupActions,
+  ActionGroupSize,
+  AllEssentialActions,
+  AllLargeGroupActions,
+  AllMediumGroupActions,
+  AllSmallGroupActions,
+  AllSupportedActions,
 } from '~/interfaces/activity';
 import type { ActivityDocument } from '~/server/models/activity';
 import Activity from '~/server/models/activity';
 
 import loggerFactory from '../../utils/logger';
 import type Crowi from '../crowi';
-
-
 import type { GeneratePreNotify, GetAdditionalTargetUsers } from './pre-notify';
 
 const logger = loggerFactory('growi:service:ActivityService');
@@ -22,12 +24,13 @@ const parseActionString = (actionsString: string): SupportedActionType[] => {
     return [];
   }
 
-  const actions = actionsString.split(',').map(value => value.trim());
-  return actions.filter(action => (AllSupportedActions as string[]).includes(action)) as SupportedActionType[];
+  const actions = actionsString.split(',').map((value) => value.trim());
+  return actions.filter((action) =>
+    (AllSupportedActions as string[]).includes(action),
+  ) as SupportedActionType[];
 };
 
 class ActivityService {
-
   crowi!: Crowi;
 
   activityEvent: any;
@@ -43,40 +46,60 @@ class ActivityService {
   }
 
   initActivityEventListeners(): void {
-    this.activityEvent.on('update', async(
-        activityId: string, parameters, target: IPage, generatePreNotify?: GeneratePreNotify, getAdditionalTargetUsers?: GetAdditionalTargetUsers,
-    ) => {
-      let activity: ActivityDocument;
-      const shoudUpdate = this.shoudUpdateActivity(parameters.action);
-
-      if (shoudUpdate) {
-        try {
-          activity = await Activity.updateByParameters(activityId, parameters);
-        }
-        catch (err) {
-          logger.error('Update activity failed', err);
-          return;
-        }
-
-        if (generatePreNotify != null) {
-          const preNotify = generatePreNotify(activity, getAdditionalTargetUsers);
-
-          this.activityEvent.emit('updated', activity, target, preNotify);
-
-          return;
+    this.activityEvent.on(
+      'update',
+      async (
+        activityId: string,
+        parameters,
+        target: IPage,
+        generatePreNotify?: GeneratePreNotify,
+        getAdditionalTargetUsers?: GetAdditionalTargetUsers,
+      ) => {
+        let activity: ActivityDocument;
+        const shoudUpdate = this.shoudUpdateActivity(parameters.action);
+
+        if (shoudUpdate) {
+          try {
+            activity = await Activity.updateByParameters(
+              activityId,
+              parameters,
+            );
+          } catch (err) {
+            logger.error('Update activity failed', err);
+            return;
+          }
+
+          if (generatePreNotify != null) {
+            const preNotify = generatePreNotify(
+              activity,
+              getAdditionalTargetUsers,
+            );
+
+            this.activityEvent.emit('updated', activity, target, preNotify);
+
+            return;
+          }
+
+          this.activityEvent.emit('updated', activity, target);
         }
-
-        this.activityEvent.emit('updated', activity, target);
-
-      }
-    });
+      },
+    );
   }
 
-  getAvailableActions = function(isIncludeEssentialActions = true): SupportedActionType[] {
-    const auditLogEnabled = this.crowi.configManager.getConfig('app:auditLogEnabled') || false;
-    const auditLogActionGroupSize = this.crowi.configManager.getConfig('app:auditLogActionGroupSize') || ActionGroupSize.Small;
-    const auditLogAdditionalActions = this.crowi.configManager.getConfig('app:auditLogAdditionalActions');
-    const auditLogExcludeActions = this.crowi.configManager.getConfig('app:auditLogExcludeActions');
+  getAvailableActions = function (
+    isIncludeEssentialActions = true,
+  ): SupportedActionType[] {
+    const auditLogEnabled =
+      this.crowi.configManager.getConfig('app:auditLogEnabled') || false;
+    const auditLogActionGroupSize =
+      this.crowi.configManager.getConfig('app:auditLogActionGroupSize') ||
+      ActionGroupSize.Small;
+    const auditLogAdditionalActions = this.crowi.configManager.getConfig(
+      'app:auditLogAdditionalActions',
+    );
+    const auditLogExcludeActions = this.crowi.configManager.getConfig(
+      'app:auditLogExcludeActions',
+    );
 
     if (!auditLogEnabled) {
       return AllEssentialActions;
@@ -87,55 +110,65 @@ class ActivityService {
     // Set base action group
     switch (auditLogActionGroupSize) {
       case ActionGroupSize.Small:
-        AllSmallGroupActions.forEach(action => availableActionsSet.add(action));
+        AllSmallGroupActions.forEach((action) =>
+          availableActionsSet.add(action),
+        );
         break;
       case ActionGroupSize.Medium:
-        AllMediumGroupActions.forEach(action => availableActionsSet.add(action));
+        AllMediumGroupActions.forEach((action) =>
+          availableActionsSet.add(action),
+        );
         break;
       case ActionGroupSize.Large:
-        AllLargeGroupActions.forEach(action => availableActionsSet.add(action));
+        AllLargeGroupActions.forEach((action) =>
+          availableActionsSet.add(action),
+        );
         break;
     }
 
     // Add additionalActions
     const additionalActions = parseActionString(auditLogAdditionalActions);
-    additionalActions.forEach(action => availableActionsSet.add(action));
+    additionalActions.forEach((action) => availableActionsSet.add(action));
 
     // Delete excludeActions
     const excludeActions = parseActionString(auditLogExcludeActions);
-    excludeActions.forEach(action => availableActionsSet.delete(action));
+    excludeActions.forEach((action) => availableActionsSet.delete(action));
 
     // Add essentialActions
     if (isIncludeEssentialActions) {
-      AllEssentialActions.forEach(action => availableActionsSet.add(action));
+      AllEssentialActions.forEach((action) => availableActionsSet.add(action));
     }
 
     return Array.from(availableActionsSet);
   };
 
-  shoudUpdateActivity = function(action: SupportedActionType): boolean {
+  shoudUpdateActivity = function (action: SupportedActionType): boolean {
     return this.getAvailableActions().includes(action);
   };
 
   // for GET request
-  createActivity = async function(parameters): Promise<IActivity | null> {
-    const shoudCreateActivity = this.crowi.activityService.shoudUpdateActivity(parameters.action);
+  createActivity = async function (parameters): Promise<IActivity | null> {
+    const shoudCreateActivity = this.crowi.activityService.shoudUpdateActivity(
+      parameters.action,
+    );
     if (shoudCreateActivity) {
       let activity: IActivity;
       try {
         activity = await Activity.createByParameters(parameters);
         return activity;
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Create activity failed', err);
       }
     }
     return null;
   };
 
-  createTtlIndex = async function() {
+  createTtlIndex = async function () {
     const configManager = this.crowi.configManager;
-    const activityExpirationSeconds = configManager != null ? configManager.getConfig('app:activityExpirationSeconds') : 2592000;
+    const activityExpirationSeconds =
+      configManager != null
+        ? configManager.getConfig('app:activityExpirationSeconds')
+        : 2592000;
 
     try {
       // create the collection with indexes at first
@@ -145,9 +178,11 @@ class ActivityService {
       const indexes = await collection.indexes();
 
       const targetField = 'createdAt_1';
-      const foundCreatedAt = indexes.find(i => i.name === targetField);
+      const foundCreatedAt = indexes.find((i) => i.name === targetField);
 
-      const isNotSpec = foundCreatedAt?.expireAfterSeconds == null || foundCreatedAt?.expireAfterSeconds !== activityExpirationSeconds;
+      const isNotSpec =
+        foundCreatedAt?.expireAfterSeconds == null ||
+        foundCreatedAt?.expireAfterSeconds !== activityExpirationSeconds;
       const shoudDropIndex = foundCreatedAt != null && isNotSpec;
       const shoudCreateIndex = foundCreatedAt == null || shoudDropIndex;
 
@@ -156,15 +191,16 @@ class ActivityService {
       }
 
       if (shoudCreateIndex) {
-        await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: activityExpirationSeconds });
+        await collection.createIndex(
+          { createdAt: 1 },
+          { expireAfterSeconds: activityExpirationSeconds },
+        );
       }
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Failed to create TTL Index', err);
       throw err;
     }
   };
-
 }
 
 module.exports = ActivityService;

+ 5 - 7
apps/app/src/server/service/app.ts

@@ -4,7 +4,6 @@ import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
 import S2sMessage from '../models/vo/s2s-message';
-
 import { configManager } from './config-manager';
 import type { S2sMessagingService } from './s2s-messaging/base';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
@@ -14,7 +13,6 @@ const logger = loggerFactory('growi:service:AppService');
  * the service class of AppService
  */
 export default class AppService implements S2sMessageHandlable {
-
   crowi: Crowi;
 
   s2sMessagingService: S2sMessagingService;
@@ -64,12 +62,13 @@ export default class AppService implements S2sMessageHandlable {
 
       try {
         await s2sMessagingService.publish(s2sMessage);
-      }
-      catch (e) {
-        logger.error('Failed to publish post installation message with S2sMessagingService: ', e.message);
+      } catch (e) {
+        logger.error(
+          'Failed to publish post installation message with S2sMessagingService: ',
+          e.message,
+        );
       }
     }
-
   }
 
   getAppTitle() {
@@ -108,5 +107,4 @@ export default class AppService implements S2sMessageHandlable {
   async endMaintenanceMode(): Promise<void> {
     await configManager.updateConfig('app:isMaintenanceMode', false);
   }
-
 }

+ 31 - 15
apps/app/src/server/service/attachment.js

@@ -12,7 +12,11 @@ const logger = loggerFactory('growi:service:AttachmentService');
 
 const createReadStream = (filePath) => {
   return fs.createReadStream(filePath, {
-    flags: 'r', encoding: null, fd: null, mode: '0666', autoClose: true,
+    flags: 'r',
+    encoding: null,
+    fd: null,
+    mode: '0666',
+    autoClose: true,
   });
 };
 
@@ -20,7 +24,6 @@ const createReadStream = (filePath) => {
  * the service class for Attachment and file-uploader
  */
 class AttachmentService {
-
   /** @type {Array<(pageId: string, attachment: Attachment, file: Express.Multer.File) => Promise<void>>} */
   attachHandlers = [];
 
@@ -35,7 +38,13 @@ class AttachmentService {
     this.crowi = crowi;
   }
 
-  async createAttachment(file, user, pageId = null, attachmentType, disposeTmpFileCallback) {
+  async createAttachment(
+    file,
+    user,
+    pageId = null,
+    attachmentType,
+    disposeTmpFileCallback,
+  ) {
     const { fileUploadService } = this.crowi;
 
     // check limit
@@ -49,8 +58,18 @@ class AttachmentService {
     let readStreamForCreateAttachmentDocument;
     try {
       readStreamForCreateAttachmentDocument = createReadStream(file.path);
-      attachment = Attachment.createWithoutSave(pageId, user, file.originalname, file.mimetype, file.size, attachmentType);
-      await fileUploadService.uploadAttachment(readStreamForCreateAttachmentDocument, attachment);
+      attachment = Attachment.createWithoutSave(
+        pageId,
+        user,
+        file.originalname,
+        file.mimetype,
+        file.size,
+        attachmentType,
+      );
+      await fileUploadService.uploadAttachment(
+        readStreamForCreateAttachmentDocument,
+        attachment,
+      );
       await attachment.save();
 
       const attachHandlerPromises = this.attachHandlers.map((handler) => {
@@ -65,13 +84,11 @@ class AttachmentService {
         .finally(() => {
           disposeTmpFileCallback?.(file);
         });
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Error while creating attachment', err);
       disposeTmpFileCallback?.(file);
       throw err;
-    }
-    finally {
+    } finally {
       readStreamForCreateAttachmentDocument.destroy();
     }
 
@@ -81,7 +98,8 @@ class AttachmentService {
   async removeAllAttachments(attachments) {
     const { fileUploadService } = this.crowi;
     const attachmentsCollection = mongoose.connection.collection('attachments');
-    const unorderAttachmentsBulkOp = attachmentsCollection.initializeUnorderedBulkOp();
+    const unorderAttachmentsBulkOp =
+      attachmentsCollection.initializeUnorderedBulkOp();
 
     if (attachments.length === 0) {
       return;
@@ -109,10 +127,9 @@ class AttachmentService {
     });
 
     // Do not await, run in background
-    Promise.all(detachedHandlerPromises)
-      .catch((err) => {
-        logger.error('Error while executing detached handler', err);
-      });
+    Promise.all(detachedHandlerPromises).catch((err) => {
+      logger.error('Error while executing detached handler', err);
+    });
 
     return;
   }
@@ -139,7 +156,6 @@ class AttachmentService {
   addDetachHandler(handler) {
     this.detachHandlers.push(handler);
   }
-
 }
 
 module.exports = AttachmentService;

+ 22 - 19
apps/app/src/server/service/comment.ts

@@ -13,7 +13,6 @@ const USERNAME_PATTERN = new RegExp(/\B@[\w@.-]+/g);
 const logger = loggerFactory('growi:service:CommentService');
 
 class CommentService {
-
   crowi!: Crowi;
 
   activityService!: any;
@@ -31,35 +30,35 @@ class CommentService {
 
   initCommentEventListeners(): void {
     // create
-    commentEvent.on(CommentEvent.CREATE, async(savedComment) => {
-
+    commentEvent.on(CommentEvent.CREATE, async (savedComment) => {
       try {
         const Page = pageModelFactory(this.crowi);
         await Page.updateCommentCount(savedComment.page);
+      } catch (err) {
+        logger.error(
+          'Error occurred while handling the comment create event:\n',
+          err,
+        );
       }
-      catch (err) {
-        logger.error('Error occurred while handling the comment create event:\n', err);
-      }
-
     });
 
     // update
-    commentEvent.on(CommentEvent.UPDATE, async() => {
-    });
+    commentEvent.on(CommentEvent.UPDATE, async () => {});
 
     // remove
-    commentEvent.on(CommentEvent.DELETE, async(removedComment) => {
+    commentEvent.on(CommentEvent.DELETE, async (removedComment) => {
       try {
         const Page = pageModelFactory(this.crowi);
         await Page.updateCommentCount(removedComment.page);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Error occurred while updating the comment count:\n', err);
       }
     });
   }
 
-  getMentionedUsers = async(commentId: Types.ObjectId): Promise<Types.ObjectId[]> => {
+  getMentionedUsers = async (
+    commentId: Types.ObjectId,
+  ): Promise<Types.ObjectId[]> => {
     const User = userModelFactory(this.crowi);
 
     // Get comment by comment ID
@@ -76,18 +75,22 @@ class CommentService {
     const usernamesFromComment = comment.match(USERNAME_PATTERN);
 
     // Get username from comment and remove duplicate username
-    const mentionedUsernames = [...new Set(usernamesFromComment?.map((username) => {
-      return username.slice(1);
-    }))];
+    const mentionedUsernames = [
+      ...new Set(
+        usernamesFromComment?.map((username) => {
+          return username.slice(1);
+        }),
+      ),
+    ];
 
     // Get mentioned users ID
-    const mentionedUserIDs = await User.find({ username: { $in: mentionedUsernames } });
+    const mentionedUserIDs = await User.find({
+      username: { $in: mentionedUsernames },
+    });
     return mentionedUserIDs?.map((user) => {
       return user._id;
     });
   };
-
 }
 
-
 module.exports = CommentService;

+ 2 - 5
apps/app/src/server/service/cron.ts

@@ -9,7 +9,6 @@ const logger = loggerFactory('growi:service:cron');
  * Base class for services that manage a cronjob
  */
 abstract class CronService {
-
   // The current cronjob to manage
   cronJob: ScheduledTask | undefined;
 
@@ -50,16 +49,14 @@ abstract class CronService {
    * @param cronSchedule e.g. '0 1 * * *'
    */
   protected generateCronJob(cronSchedule: string): ScheduledTask {
-    return nodeCron.schedule(cronSchedule, async() => {
+    return nodeCron.schedule(cronSchedule, async () => {
       try {
         await this.executeJob();
-      }
-      catch (e) {
+      } catch (e) {
         logger.error(e);
       }
     });
   }
-
 }
 
 export default CronService;

+ 36 - 22
apps/app/src/server/service/customize.ts

@@ -1,8 +1,11 @@
-import path from 'path';
-
 import type { ColorScheme } from '@growi/core';
 import { getForcedColorScheme } from '@growi/core/dist/utils';
-import { DefaultThemeMetadata, PresetThemesMetadatas, manifestPath } from '@growi/preset-themes';
+import {
+  DefaultThemeMetadata,
+  manifestPath,
+  PresetThemesMetadatas,
+} from '@growi/preset-themes';
+import path from 'path';
 import uglifycss from 'uglifycss';
 
 import { growiPluginService } from '~/features/growi-plugin/server/services';
@@ -10,20 +13,15 @@ import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
 import S2sMessage from '../models/vo/s2s-message';
-
-
 import { configManager } from './config-manager';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
-
 const logger = loggerFactory('growi:service:CustomizeService');
 
-
 /**
  * the service class of CustomizeService
  */
 class CustomizeService implements S2sMessageHandlable {
-
   s2sMessagingService: any;
 
   appService: any;
@@ -54,7 +52,10 @@ class CustomizeService implements S2sMessageHandlable {
       return false;
     }
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
+    return (
+      this.lastLoadedAt == null ||
+      this.lastLoadedAt < new Date(s2sMessage.updatedAt)
+    );
   }
 
   /**
@@ -72,13 +73,17 @@ class CustomizeService implements S2sMessageHandlable {
     const { s2sMessagingService } = this;
 
     if (s2sMessagingService != null) {
-      const s2sMessage = new S2sMessage('customizeServiceUpdated', { updatedAt: new Date() });
+      const s2sMessage = new S2sMessage('customizeServiceUpdated', {
+        updatedAt: new Date(),
+      });
 
       try {
         await s2sMessagingService.publish(s2sMessage);
-      }
-      catch (e) {
-        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      } catch (e) {
+        logger.error(
+          'Failed to publish update message with S2sMessagingService: ',
+          e.message,
+        );
       }
     }
   }
@@ -124,29 +129,38 @@ class CustomizeService implements S2sMessageHandlable {
 
     this.theme = theme;
 
-    const resultForThemePlugin = await growiPluginService.findThemePlugin(theme);
+    const resultForThemePlugin =
+      await growiPluginService.findThemePlugin(theme);
 
     if (resultForThemePlugin != null) {
-      this.forcedColorScheme = getForcedColorScheme(resultForThemePlugin.themeMetadata.schemeType);
+      this.forcedColorScheme = getForcedColorScheme(
+        resultForThemePlugin.themeMetadata.schemeType,
+      );
       this.themeHref = resultForThemePlugin.themeHref;
     }
     // retrieve preset theme
     else {
       // import preset-themes manifest
-      const presetThemesManifest = await import(path.join('@growi/preset-themes', manifestPath)).then(imported => imported.default);
+      const presetThemesManifest = await import(
+        path.join('@growi/preset-themes', manifestPath)
+      ).then((imported) => imported.default);
 
-      const themeMetadata = PresetThemesMetadatas.find(p => p.name === theme);
+      const themeMetadata = PresetThemesMetadatas.find((p) => p.name === theme);
       this.forcedColorScheme = getForcedColorScheme(themeMetadata?.schemeType);
 
-      const manifestKey = themeMetadata?.manifestKey ?? DefaultThemeMetadata.manifestKey;
-      if (themeMetadata == null || !(themeMetadata.manifestKey in presetThemesManifest)) {
-        logger.warn(`Use default theme because the key for '${theme} does not exist in preset-themes manifest`);
+      const manifestKey =
+        themeMetadata?.manifestKey ?? DefaultThemeMetadata.manifestKey;
+      if (
+        themeMetadata == null ||
+        !(themeMetadata.manifestKey in presetThemesManifest)
+      ) {
+        logger.warn(
+          `Use default theme because the key for '${theme} does not exist in preset-themes manifest`,
+        );
       }
       this.themeHref = `/static/preset-themes/${presetThemesManifest[manifestKey].file}`; // configured by express.static
     }
-
   }
-
 }
 
 module.exports = CustomizeService;

+ 55 - 30
apps/app/src/server/service/export.ts

@@ -1,45 +1,41 @@
+import archiver from 'archiver';
 import fs from 'fs';
 import path from 'path';
 import { Readable, Transform } from 'stream';
 
-import archiver from 'archiver';
-
 import { toArrayIfNot } from '~/utils/array-utils';
 import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 
 import type CollectionProgress from '../models/vo/collection-progress';
 import CollectionProgressingStatus from '../models/vo/collection-progressing-status';
-
 import type AppService from './app';
 import { configManager } from './config-manager';
 import type { GrowiBridgeService } from './growi-bridge';
 import { growiInfoService } from './growi-info';
 import type { ZipFileStat } from './interfaces/export';
 
-
 const logger = loggerFactory('growi:services:ExportService');
 const { pipeline, finished } = require('stream/promises');
 
 const mongoose = require('mongoose');
 
 class ExportProgressingStatus extends CollectionProgressingStatus {
-
   async init() {
     // retrieve total document count from each collections
-    const promises = this.progressList.map(async(collectionProgress) => {
-      const collection = mongoose.connection.collection(collectionProgress.collectionName);
+    const promises = this.progressList.map(async (collectionProgress) => {
+      const collection = mongoose.connection.collection(
+        collectionProgress.collectionName,
+      );
       collectionProgress.totalCount = await collection.count();
     });
     await Promise.all(promises);
 
     this.recalculateTotalCount();
   }
-
 }
 
 class ExportService {
-
   crowi: any;
 
   appService: AppService;
@@ -83,7 +79,9 @@ class ExportService {
    * @return {object} info for zip files and whether currentProgressingStatus exists
    */
   async getStatus() {
-    const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
+    const zipFiles = fs
+      .readdirSync(this.baseDir)
+      .filter((file) => path.extname(file) === '.zip');
 
     // process serially so as not to waste memory
     const zipFileStats: Array<ZipFileStat | null> = [];
@@ -96,14 +94,16 @@ class ExportService {
     }
 
     // filter null object (broken zip)
-    const filtered = zipFileStats.filter(element => element != null);
+    const filtered = zipFileStats.filter((element) => element != null);
 
     const isExporting = this.currentProgressingStatus != null;
 
     return {
       zipFileStats: filtered,
       isExporting,
-      progressList: isExporting ? this.currentProgressingStatus?.progressList : null,
+      progressList: isExporting
+        ? this.currentProgressingStatus?.progressList
+        : null,
     };
   }
 
@@ -114,8 +114,13 @@ class ExportService {
    * @return {string} path to meta.json
    */
   async createMetaJson(): Promise<string> {
-    const metaJson = path.join(this.baseDir, this.growiBridgeService.getMetaFileName());
-    const writeStream = fs.createWriteStream(metaJson, { encoding: this.growiBridgeService.getEncoding() });
+    const metaJson = path.join(
+      this.baseDir,
+      this.growiBridgeService.getMetaFileName(),
+    );
+    const writeStream = fs.createWriteStream(metaJson, {
+      encoding: this.growiBridgeService.getEncoding(),
+    });
     const passwordSeed = this.crowi.env.PASSWORD_SEED || null;
 
     const metaData = {
@@ -211,12 +216,15 @@ class ExportService {
     const transformStream = this.generateTransformStream();
 
     // log configuration
-    const exportProgress = this.currentProgressingStatus?.progressMap[collectionName];
+    const exportProgress =
+      this.currentProgressingStatus?.progressMap[collectionName];
     const logStream = this.generateLogStream(exportProgress);
 
     // create WritableStream
     const jsonFileToWrite = path.join(this.baseDir, `${collectionName}.json`);
-    const writeStream = fs.createWriteStream(jsonFileToWrite, { encoding: this.growiBridgeService.getEncoding() });
+    const writeStream = fs.createWriteStream(jsonFileToWrite, {
+      encoding: this.growiBridgeService.getEncoding(),
+    });
 
     await pipeline(readStream, logStream, transformStream, writeStream);
 
@@ -230,12 +238,16 @@ class ExportService {
    * @param {Array.<string>} collections array of collection name
    * @return {Array.<ZipFileStat>} info of zip file created
    */
-  async exportCollectionsToZippedJson(collections: string[]): Promise<ZipFileStat | null> {
+  async exportCollectionsToZippedJson(
+    collections: string[],
+  ): Promise<ZipFileStat | null> {
     const metaJson = await this.createMetaJson();
 
     // process serially so as not to waste memory
     const jsonFiles: string[] = [];
-    const jsonFilesPromises = collections.map(collectionName => this.exportCollectionToJson(collectionName));
+    const jsonFilesPromises = collections.map((collectionName) =>
+      this.exportCollectionToJson(collectionName),
+    );
     for await (const jsonFile of jsonFilesPromises) {
       jsonFiles.push(jsonFile);
     }
@@ -244,14 +256,17 @@ class ExportService {
     this.emitStartZippingEvent();
 
     // zip json
-    const configs = jsonFiles.map((jsonFile) => { return { from: jsonFile, as: path.basename(jsonFile) } });
+    const configs = jsonFiles.map((jsonFile) => {
+      return { from: jsonFile, as: path.basename(jsonFile) };
+    });
     // add meta.json in zip
     configs.push({ from: metaJson, as: path.basename(metaJson) });
     // exec zip
     const zipFile = await this.zipFiles(configs);
 
     // get stats for the zip file
-    const addedZipFileStat = await this.growiBridgeService.parseZipFile(zipFile);
+    const addedZipFileStat =
+      await this.growiBridgeService.parseZipFile(zipFile);
 
     // send terminate event
     this.emitTerminateEvent(addedZipFileStat);
@@ -272,8 +287,7 @@ class ExportService {
     let zipFileStat: ZipFileStat | null;
     try {
       zipFileStat = await this.exportCollectionsToZippedJson(collections);
-    }
-    finally {
+    } finally {
       this.currentProgressingStatus = null;
     }
 
@@ -288,7 +302,10 @@ class ExportService {
    * @param {CollectionProgress} collectionProgress
    * @param {number} currentCount number of items exported
    */
-  logProgress(collectionProgress: CollectionProgress | undefined, currentCount: number): void {
+  logProgress(
+    collectionProgress: CollectionProgress | undefined,
+    currentCount: number,
+  ): void {
     if (collectionProgress == null) return;
 
     const output = `${collectionProgress.collectionName}: ${currentCount}/${collectionProgress.totalCount} written`;
@@ -334,7 +351,9 @@ class ExportService {
    * @param {object} zipFileStat added zip file status data
    */
   emitTerminateEvent(zipFileStat: ZipFileStat | null): void {
-    this.adminEvent.emit('onTerminateForExport', { addedZipFileStat: zipFileStat });
+    this.adminEvent.emit('onTerminateForExport', {
+      addedZipFileStat: zipFileStat,
+    });
   }
 
   /**
@@ -345,11 +364,14 @@ class ExportService {
    * @return {string} absolute path to the zip file
    * @see https://www.archiverjs.com/#quick-start
    */
-  async zipFiles(_configs: {from: string, as: string}[]): Promise<string> {
+  async zipFiles(_configs: { from: string; as: string }[]): Promise<string> {
     const configs = toArrayIfNot(_configs);
     const appTitle = this.appService.getAppTitle();
-    const timeStamp = (new Date()).getTime();
-    const zipFile = path.join(this.baseDir, `${appTitle}-${timeStamp}.growi.zip`);
+    const timeStamp = new Date().getTime();
+    const zipFile = path.join(
+      this.baseDir,
+      `${appTitle}-${timeStamp}.growi.zip`,
+    );
     const archive = archiver('zip', {
       zlib: { level: this.zlibLevel },
     });
@@ -361,7 +383,9 @@ class ExportService {
     });
 
     // good practice to catch this error explicitly
-    archive.on('error', (err) => { throw err });
+    archive.on('error', (err) => {
+      throw err;
+    });
 
     for (const { from, as } of configs) {
       const input = fs.createReadStream(from);
@@ -379,7 +403,9 @@ class ExportService {
     // pipe archive data to the file
     await pipeline(archive, output);
 
-    logger.info(`zipped GROWI data into ${zipFile} (${archive.pointer()} bytes)`);
+    logger.info(
+      `zipped GROWI data into ${zipFile} (${archive.pointer()} bytes)`,
+    );
 
     // delete json files
     for (const { from } of configs) {
@@ -399,7 +425,6 @@ class ExportService {
 
     return readable;
   }
-
 }
 
 // eslint-disable-next-line import/no-mutable-exports

+ 24 - 19
apps/app/src/server/service/external-account.ts

@@ -7,13 +7,11 @@ import loggerFactory from '~/utils/logger';
 import { NullUsernameToBeRegisteredError } from '../models/errors';
 import type { ExternalAccountDocument } from '../models/external-account';
 import ExternalAccount from '../models/external-account';
-
 import type PassportService from './passport';
 
 const logger = loggerFactory('growi:service:external-account-service');
 
 class ExternalAccountService {
-
   passportService: PassportService;
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -22,14 +20,16 @@ class ExternalAccountService {
   }
 
   async getOrCreateUser(
-      userInfo: {id: string, username: string, name?: string, email?: string},
-      providerId: IExternalAuthProviderType,
+    userInfo: { id: string; username: string; name?: string; email?: string },
+    providerId: IExternalAuthProviderType,
   ): Promise<ExternalAccountDocument | undefined> {
     // get option
-    const isSameUsernameTreatedAsIdenticalUser = this.passportService.isSameUsernameTreatedAsIdenticalUser(providerId);
-    const isSameEmailTreatedAsIdenticalUser = providerId === 'ldap'
-      ? false
-      : this.passportService.isSameEmailTreatedAsIdenticalUser(providerId);
+    const isSameUsernameTreatedAsIdenticalUser =
+      this.passportService.isSameUsernameTreatedAsIdenticalUser(providerId);
+    const isSameEmailTreatedAsIdenticalUser =
+      providerId === 'ldap'
+        ? false
+        : this.passportService.isSameEmailTreatedAsIdenticalUser(providerId);
 
     try {
       // find or register(create) user
@@ -43,30 +43,35 @@ class ExternalAccountService {
         userInfo.email,
       );
       return externalAccount;
-    }
-    catch (err) {
+    } catch (err) {
       if (err instanceof NullUsernameToBeRegisteredError) {
         logger.error(err.message);
         throw new ErrorV3(err.message);
-      }
-      else if (err.name === 'DuplicatedUsernameException') {
-        if (isSameEmailTreatedAsIdenticalUser || isSameUsernameTreatedAsIdenticalUser) {
+      } else if (err.name === 'DuplicatedUsernameException') {
+        if (
+          isSameEmailTreatedAsIdenticalUser ||
+          isSameUsernameTreatedAsIdenticalUser
+        ) {
           // associate to existing user
-          logger.debug(`ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`);
+          logger.debug(
+            `ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`,
+          );
           return ExternalAccount.associate(providerId, userInfo.id, err.user);
         }
         logger.error('provider-DuplicatedUsernameException', providerId);
 
-        throw new ErrorV3('message.provider_duplicated_username_exception', LoginErrorCode.PROVIDER_DUPLICATED_USERNAME_EXCEPTION,
-          undefined, { failedProviderForDuplicatedUsernameException: providerId });
-      }
-      else if (err.name === 'UserUpperLimitException') {
+        throw new ErrorV3(
+          'message.provider_duplicated_username_exception',
+          LoginErrorCode.PROVIDER_DUPLICATED_USERNAME_EXCEPTION,
+          undefined,
+          { failedProviderForDuplicatedUsernameException: providerId },
+        );
+      } else if (err.name === 'UserUpperLimitException') {
         logger.error(err.message);
         throw new ErrorV3(err.message);
       }
     }
   }
-
 }
 
 // eslint-disable-next-line import/no-mutable-exports

+ 12 - 8
apps/app/src/server/service/file-uploader-switch.ts

@@ -2,7 +2,6 @@ import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
 import S2sMessage from '../models/vo/s2s-message';
-
 import { configManager } from './config-manager';
 import type { S2sMessagingService } from './s2s-messaging/base';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
@@ -10,7 +9,6 @@ import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 const logger = loggerFactory('growi:service:FileUploaderSwitch');
 
 class FileUploaderSwitch implements S2sMessageHandlable {
-
   crowi: Crowi;
 
   s2sMessagingService: S2sMessagingService;
@@ -31,7 +29,10 @@ class FileUploaderSwitch implements S2sMessageHandlable {
       return false;
     }
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
+    return (
+      this.lastLoadedAt == null ||
+      this.lastLoadedAt < new Date(s2sMessage.updatedAt)
+    );
   }
 
   /**
@@ -47,17 +48,20 @@ class FileUploaderSwitch implements S2sMessageHandlable {
     const { s2sMessagingService } = this;
 
     if (s2sMessagingService != null) {
-      const s2sMessage = new S2sMessage('fileUploadServiceUpdated', { updatedAt: new Date() });
+      const s2sMessage = new S2sMessage('fileUploadServiceUpdated', {
+        updatedAt: new Date(),
+      });
 
       try {
         await s2sMessagingService.publish(s2sMessage);
-      }
-      catch (e) {
-        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      } catch (e) {
+        logger.error(
+          'Failed to publish update message with S2sMessagingService: ',
+          e.message,
+        );
       }
     }
   }
-
 }
 
 module.exports = FileUploaderSwitch;

+ 209 - 108
apps/app/src/server/service/g2g-transfer.ts

@@ -1,13 +1,12 @@
-import type { ReadStream } from 'fs';
-import { createReadStream } from 'fs';
-import { basename } from 'path';
-import type { Readable } from 'stream';
-
 import { ConfigSource } from '@growi/core';
 import type { IUser } from '@growi/core/dist/interfaces';
 import rawAxios, { type AxiosRequestConfig } from 'axios';
 import FormData from 'form-data';
+import type { ReadStream } from 'fs';
+import { createReadStream } from 'fs';
 import mongoose, { Types as MongooseTypes } from 'mongoose';
+import { basename } from 'path';
+import type { Readable } from 'stream';
 
 import { G2G_PROGRESS_STATUS } from '~/interfaces/g2g-transfer';
 import { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option';
@@ -22,8 +21,10 @@ import { TransferKey } from '~/utils/vo/transfer-key';
 
 import type Crowi from '../crowi';
 import { Attachment } from '../models/attachment';
-import { G2GTransferError, G2GTransferErrorCode } from '../models/vo/g2g-transfer-error';
-
+import {
+  G2GTransferError,
+  G2GTransferErrorCode,
+} from '../models/vo/g2g-transfer-error';
 import { configManager } from './config-manager';
 import type { ConfigKey } from './config-manager/config-definition';
 import { exportService } from './export';
@@ -58,20 +59,20 @@ const UPLOAD_CONFIG_KEYS = [
 /**
  * File upload related configs
  */
-type FileUploadConfigs = { [key in typeof UPLOAD_CONFIG_KEYS[number] ]: any; }
+type FileUploadConfigs = { [key in (typeof UPLOAD_CONFIG_KEYS)[number]]: any };
 
 /**
  * Data used for comparing to/from GROWI information
  */
 export type IDataGROWIInfo = {
   /** GROWI version */
-  version: string
+  version: string;
   /** Max user count */
-  userUpperLimit: number | null // Handle null as Infinity
+  userUpperLimit: number | null; // Handle null as Infinity
   /** Whether file upload is disabled */
   fileUploadDisabled: boolean;
   /** Total file size allowed */
-  fileUploadTotalLimit: number | null // Handle null as Infinity
+  fileUploadTotalLimit: number | null; // Handle null as Infinity
   /** Attachment infromation */
   attachmentInfo: {
     /** File storage type */
@@ -89,7 +90,7 @@ export type IDataGROWIInfo = {
     /** Azure container name */
     containerName?: string;
   };
-}
+};
 
 /**
  * File metadata in storage
@@ -105,7 +106,9 @@ interface FileMeta {
 /**
  * Return type for {@link Pusher.getTransferability}
  */
-type Transferability = { canTransfer: true; } | { canTransfer: false; reason: string; };
+type Transferability =
+  | { canTransfer: true }
+  | { canTransfer: false; reason: string };
 
 /**
  * G2g transfer pusher
@@ -116,27 +119,30 @@ interface Pusher {
    * @param {TransferKey} tk Transfer key
    * @param {AxiosRequestConfig} config Axios config
    */
-  generateAxiosConfig(tk: TransferKey, config: AxiosRequestConfig): AxiosRequestConfig
+  generateAxiosConfig(
+    tk: TransferKey,
+    config: AxiosRequestConfig,
+  ): AxiosRequestConfig;
   /**
    * Send to-growi a request to get GROWI info
    * @param {TransferKey} tk Transfer key
    */
-  askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo>
+  askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo>;
   /**
    * Check if transfering is proceedable
    * @param {IDataGROWIInfo} destGROWIInfo GROWI info from dest GROWI
    */
-  getTransferability(destGROWIInfo: IDataGROWIInfo): Promise<Transferability>
+  getTransferability(destGROWIInfo: IDataGROWIInfo): Promise<Transferability>;
   /**
    * List files in the storage
    * @param {TransferKey} tk Transfer key
    */
-  listFilesInStorage(tk: TransferKey): Promise<FileMeta[]>
+  listFilesInStorage(tk: TransferKey): Promise<FileMeta[]>;
   /**
    * Transfer all Attachment data to dest GROWI
    * @param {TransferKey} tk Transfer key
    */
-  transferAttachments(tk: TransferKey): Promise<void>
+  transferAttachments(tk: TransferKey): Promise<void>;
   /**
    * Start transfer data between GROWIs
    * @param {TransferKey} tk TransferKey object
@@ -151,7 +157,7 @@ interface Pusher {
     collections: string[],
     optionsMap: any,
     destGROWIInfo: IDataGROWIInfo,
-  ): Promise<void>
+  ): Promise<void>;
 }
 
 /**
@@ -163,12 +169,12 @@ interface Receiver {
    * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
    * @param {string} key Transfer key
    */
-  validateTransferKey(key: string): Promise<void>
+  validateTransferKey(key: string): Promise<void>;
   /**
    * Generate GROWIInfo
    * @throws {import('../models/vo/g2g-transfer-error').G2GTransferError}
    */
-  answerGROWIInfo(): Promise<IDataGROWIInfo>
+  answerGROWIInfo(): Promise<IDataGROWIInfo>;
   /**
    * DO NOT USE TransferKeyModel.create() directly, instead, use this method to create a TransferKey document.
    * This method receives appSiteUrlOrigin to create a TransferKey document and returns generated transfer key string.
@@ -176,7 +182,7 @@ interface Receiver {
    * @param {string} appSiteUrlOrigin GROWI app site URL origin
    * @returns {string} Transfer key string (e.g. http://localhost:3000__grw_internal_tranferkey__<uuid>)
    */
-  createTransferKey(appSiteUrlOrigin: string): Promise<string>
+  createTransferKey(appSiteUrlOrigin: string): Promise<string>;
   /**
    * Returns a map of collection name and ImportSettings
    * @param {any[]} innerFileStats
@@ -186,9 +192,9 @@ interface Receiver {
    */
   getImportSettingMap(
     innerFileStats: any[],
-    optionsMap: { [key: string]: GrowiArchiveImportOption; },
+    optionsMap: { [key: string]: GrowiArchiveImportOption },
     operatorUserId: string,
-  ): Map<string, ImportSettings>
+  ): Map<string, ImportSettings>;
   /**
    * Import collections
    * @param {string} collections Array of collection name
@@ -199,29 +205,28 @@ interface Receiver {
     collections: string[],
     importSettingsMap: Map<string, ImportSettings>,
     sourceGROWIUploadConfigs: FileUploadConfigs,
-  ): Promise<void>
+  ): Promise<void>;
   /**
    * Returns file upload configs
    */
-  getFileUploadConfigs(): Promise<FileUploadConfigs>
-    /**
+  getFileUploadConfigs(): Promise<FileUploadConfigs>;
+  /**
    * Update file upload configs
    * @param fileUploadConfigs  File upload configs
    */
-  updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void>
+  updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void>;
   /**
    * Upload attachment file
    * @param {ReadStream} content Pushed attachment data from source GROWI
    * @param {any} attachmentMap Map-ped Attachment instance
    */
-  receiveAttachment(content: ReadStream, attachmentMap: any): Promise<void>
+  receiveAttachment(content: ReadStream, attachmentMap: any): Promise<void>;
 }
 
 /**
  * G2g transfer pusher
  */
 export class G2GTransferPusherService implements Pusher {
-
   crowi: Crowi;
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -229,7 +234,10 @@ export class G2GTransferPusherService implements Pusher {
     this.crowi = crowi;
   }
 
-  public generateAxiosConfig(tk: TransferKey, baseConfig: AxiosRequestConfig = {}): AxiosRequestConfig {
+  public generateAxiosConfig(
+    tk: TransferKey,
+    baseConfig: AxiosRequestConfig = {},
+  ): AxiosRequestConfig {
     const { appSiteUrlOrigin, key } = tk;
 
     return {
@@ -245,16 +253,25 @@ export class G2GTransferPusherService implements Pusher {
 
   public async askGROWIInfo(tk: TransferKey): Promise<IDataGROWIInfo> {
     try {
-      const { data: { growiInfo } } = await axios.get('/_api/v3/g2g-transfer/growi-info', this.generateAxiosConfig(tk));
+      const {
+        data: { growiInfo },
+      } = await axios.get(
+        '/_api/v3/g2g-transfer/growi-info',
+        this.generateAxiosConfig(tk),
+      );
       return growiInfo;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
-      throw new G2GTransferError('Failed to retrieve GROWI info.', G2GTransferErrorCode.FAILED_TO_RETRIEVE_GROWI_INFO);
+      throw new G2GTransferError(
+        'Failed to retrieve GROWI info.',
+        G2GTransferErrorCode.FAILED_TO_RETRIEVE_GROWI_INFO,
+      );
     }
   }
 
-  public async getTransferability(destGROWIInfo: IDataGROWIInfo): Promise<Transferability> {
+  public async getTransferability(
+    destGROWIInfo: IDataGROWIInfo,
+  ): Promise<Transferability> {
     const { fileUploadService } = this.crowi;
 
     const version = getGrowiVersion();
@@ -325,12 +342,19 @@ export class G2GTransferPusherService implements Pusher {
 
   public async listFilesInStorage(tk: TransferKey): Promise<FileMeta[]> {
     try {
-      const { data: { files } } = await axios.get<{ files: FileMeta[] }>('/_api/v3/g2g-transfer/files', this.generateAxiosConfig(tk));
+      const {
+        data: { files },
+      } = await axios.get<{ files: FileMeta[] }>(
+        '/_api/v3/g2g-transfer/files',
+        this.generateAxiosConfig(tk),
+      );
       return files;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
-      throw new G2GTransferError('Failed to retrieve file metadata', G2GTransferErrorCode.FAILED_TO_RETRIEVE_FILE_METADATA);
+      throw new G2GTransferError(
+        'Failed to retrieve file metadata',
+        G2GTransferErrorCode.FAILED_TO_RETRIEVE_FILE_METADATA,
+      );
     }
   }
 
@@ -381,14 +405,17 @@ export class G2GTransferPusherService implements Pusher {
      * | c.png | 1024 |
      * | d.png | 2048 |
      */
-    const filter = filesFromSrcGROWI.length > 0 ? {
-      $and: filesFromSrcGROWI.map(({ name, size }) => ({
-        $or: [
-          { fileName: { $ne: basename(name) } },
-          { fileSize: { $ne: size } },
-        ],
-      })),
-    } : {};
+    const filter =
+      filesFromSrcGROWI.length > 0
+        ? {
+            $and: filesFromSrcGROWI.map(({ name, size }) => ({
+              $or: [
+                { fileName: { $ne: basename(name) } },
+                { fileSize: { $ne: size } },
+              ],
+            })),
+          }
+        : {};
     const attachmentsCursor = await Attachment.find(filter).cursor();
     const batchStream = createBatchStream(BATCH_SIZE);
 
@@ -399,9 +426,11 @@ export class G2GTransferPusherService implements Pusher {
         try {
           // get read stream of each attachment
           fileStream = await fileUploadService.findDeliveryFile(attachment);
-        }
-        catch (err) {
-          logger.warn(`Error occured when getting Attachment(ID=${attachment.id}), skipping: `, err);
+        } catch (err) {
+          logger.warn(
+            `Error occured when getting Attachment(ID=${attachment.id}), skipping: `,
+            err,
+          );
           socket?.emit('admin:g2gError', {
             message: `Error occured when uploading Attachment(ID=${attachment.id})`,
             key: `Error occured when uploading Attachment(ID=${attachment.id})`,
@@ -413,9 +442,11 @@ export class G2GTransferPusherService implements Pusher {
         // post each attachment file data to receiver
         try {
           await this.doTransferAttachment(tk, attachment, fileStream);
-        }
-        catch (err) {
-          logger.error(`Error occured when uploading attachment(ID=${attachment.id})`, err);
+        } catch (err) {
+          logger.error(
+            `Error occured when uploading attachment(ID=${attachment.id})`,
+            err,
+          );
           socket?.emit('admin:g2gError', {
             message: `Error occured when uploading Attachment(ID=${attachment.id})`,
             key: `Error occured when uploading Attachment(ID=${attachment.id})`,
@@ -428,7 +459,13 @@ export class G2GTransferPusherService implements Pusher {
   }
 
   // eslint-disable-next-line max-len
-  public async startTransfer(tk: TransferKey, user: any, collections: string[], optionsMap: any, destGROWIInfo: IDataGROWIInfo): Promise<void> {
+  public async startTransfer(
+    tk: TransferKey,
+    user: any,
+    collections: string[],
+    optionsMap: any,
+    destGROWIInfo: IDataGROWIInfo,
+  ): Promise<void> {
     const socket = this.crowi.socketIoService?.getAdminSocket();
 
     socket?.emit('admin:g2gProgress', {
@@ -438,9 +475,11 @@ export class G2GTransferPusherService implements Pusher {
 
     const targetConfigKeys = UPLOAD_CONFIG_KEYS;
 
-    const uploadConfigs = Object.fromEntries(targetConfigKeys.map((key) => {
-      return [key, configManager.getConfig(key)];
-    }));
+    const uploadConfigs = Object.fromEntries(
+      targetConfigKeys.map((key) => {
+        return [key, configManager.getConfig(key)];
+      }),
+    );
 
     let zipFileStream: ReadStream;
     try {
@@ -450,14 +489,16 @@ export class G2GTransferPusherService implements Pusher {
       if (zipFilePath == null) throw new Error('Failed to generate zip file');
 
       zipFileStream = createReadStream(zipFilePath);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       socket?.emit('admin:g2gProgress', {
         mongo: G2G_PROGRESS_STATUS.ERROR,
         attachments: G2G_PROGRESS_STATUS.PENDING,
       });
-      socket?.emit('admin:g2gError', { message: 'Failed to generate GROWI archive file', key: 'admin:g2g:error_generate_growi_archive' });
+      socket?.emit('admin:g2gError', {
+        message: 'Failed to generate GROWI archive file',
+        key: 'admin:g2g:error_generate_growi_archive',
+      });
       throw err;
     }
 
@@ -467,20 +508,30 @@ export class G2GTransferPusherService implements Pusher {
       const form = new FormData();
 
       const appTitle = this.crowi.appService.getAppTitle();
-      form.append('transferDataZipFile', zipFileStream, `${appTitle}-${Date.now}.growi.zip`);
+      form.append(
+        'transferDataZipFile',
+        zipFileStream,
+        `${appTitle}-${Date.now}.growi.zip`,
+      );
       form.append('collections', JSON.stringify(collections));
       form.append('optionsMap', JSON.stringify(optionsMap));
       form.append('operatorUserId', user._id.toString());
       form.append('uploadConfigs', JSON.stringify(uploadConfigs));
-      await rawAxios.post('/_api/v3/g2g-transfer/', form, this.generateAxiosConfig(tk, { headers: form.getHeaders() }));
-    }
-    catch (err) {
+      await rawAxios.post(
+        '/_api/v3/g2g-transfer/',
+        form,
+        this.generateAxiosConfig(tk, { headers: form.getHeaders() }),
+      );
+    } catch (err) {
       logger.error(err);
       socket?.emit('admin:g2gProgress', {
         mongo: G2G_PROGRESS_STATUS.ERROR,
         attachments: G2G_PROGRESS_STATUS.PENDING,
       });
-      socket?.emit('admin:g2gError', { message: 'Failed to send GROWI archive file to the destination GROWI', key: 'admin:g2g:error_send_growi_archive' });
+      socket?.emit('admin:g2gError', {
+        message: 'Failed to send GROWI archive file to the destination GROWI',
+        key: 'admin:g2g:error_send_growi_archive',
+      });
       throw err;
     }
 
@@ -491,14 +542,16 @@ export class G2GTransferPusherService implements Pusher {
 
     try {
       await this.transferAttachments(tk);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       socket?.emit('admin:g2gProgress', {
         mongo: G2G_PROGRESS_STATUS.COMPLETED,
         attachments: G2G_PROGRESS_STATUS.ERROR,
       });
-      socket?.emit('admin:g2gError', { message: 'Failed to transfer attachments', key: 'admin:g2g:error_upload_attachment' });
+      socket?.emit('admin:g2gError', {
+        message: 'Failed to transfer attachments',
+        key: 'admin:g2g:error_upload_attachment',
+      });
       throw err;
     }
 
@@ -514,22 +567,28 @@ export class G2GTransferPusherService implements Pusher {
    * @param {any} attachment Attachment model instance
    * @param {Readable} fileStream Attachment data(loaded from storage)
    */
-  private async doTransferAttachment(tk: TransferKey, attachment: any, fileStream: Readable): Promise<void> {
+  private async doTransferAttachment(
+    tk: TransferKey,
+    attachment: any,
+    fileStream: Readable,
+  ): Promise<void> {
     // Use FormData to immitate browser's form data object
     const form = new FormData();
 
     form.append('content', fileStream, attachment.fileName);
     form.append('attachmentMetadata', JSON.stringify(attachment));
-    await rawAxios.post('/_api/v3/g2g-transfer/attachment', form, this.generateAxiosConfig(tk, { headers: form.getHeaders() }));
+    await rawAxios.post(
+      '/_api/v3/g2g-transfer/attachment',
+      form,
+      this.generateAxiosConfig(tk, { headers: form.getHeaders() }),
+    );
   }
-
 }
 
 /**
  * G2g transfer receiver
  */
 export class G2GTransferReceiverService implements Receiver {
-
   crowi: Crowi;
 
   constructor(crowi: Crowi) {
@@ -545,8 +604,7 @@ export class G2GTransferReceiverService implements Receiver {
 
     try {
       TransferKey.parse(transferKey.keyString);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error(`Transfer key "${key}" is invalid`);
     }
@@ -556,7 +614,9 @@ export class G2GTransferReceiverService implements Receiver {
     const { fileUploadService } = this.crowi;
     const version = getGrowiVersion();
     const userUpperLimit = configManager.getConfig('security:userUpperLimit');
-    const fileUploadDisabled = configManager.getConfig('app:fileUploadDisabled');
+    const fileUploadDisabled = configManager.getConfig(
+      'app:fileUploadDisabled',
+    );
     const fileUploadTotalLimit = fileUploadService.getFileUploadTotalLimit();
     const isWritable = await fileUploadService.isWritable();
 
@@ -574,15 +634,23 @@ export class G2GTransferReceiverService implements Receiver {
     switch (attachmentInfo.type) {
       case 'aws':
         attachmentInfo.bucket = configManager.getConfig('aws:s3Bucket');
-        attachmentInfo.customEndpoint = configManager.getConfig('aws:s3CustomEndpoint');
+        attachmentInfo.customEndpoint = configManager.getConfig(
+          'aws:s3CustomEndpoint',
+        );
         break;
       case 'gcs':
         attachmentInfo.bucket = configManager.getConfig('gcs:bucket');
-        attachmentInfo.uploadNamespace = configManager.getConfig('gcs:uploadNamespace');
+        attachmentInfo.uploadNamespace = configManager.getConfig(
+          'gcs:uploadNamespace',
+        );
         break;
       case 'azure':
-        attachmentInfo.accountName = configManager.getConfig('azure:storageAccountName');
-        attachmentInfo.containerName = configManager.getConfig('azure:storageContainerName');
+        attachmentInfo.accountName = configManager.getConfig(
+          'azure:storageAccountName',
+        );
+        attachmentInfo.containerName = configManager.getConfig(
+          'azure:storageContainerName',
+        );
         break;
       default:
     }
@@ -598,14 +666,20 @@ export class G2GTransferReceiverService implements Receiver {
 
   public async createTransferKey(appSiteUrlOrigin: string): Promise<string> {
     const uuid = new MongooseTypes.ObjectId().toString();
-    const transferKeyString = TransferKey.generateKeyString(uuid, appSiteUrlOrigin);
+    const transferKeyString = TransferKey.generateKeyString(
+      uuid,
+      appSiteUrlOrigin,
+    );
 
     // Save TransferKey document
     let tkd;
     try {
-      tkd = await TransferKeyModel.create({ _id: uuid, keyString: transferKeyString, key: uuid });
-    }
-    catch (err) {
+      tkd = await TransferKeyModel.create({
+        _id: uuid,
+        keyString: transferKeyString,
+        key: uuid,
+      });
+    } catch (err) {
       logger.error(err);
       throw err;
     }
@@ -614,31 +688,50 @@ export class G2GTransferReceiverService implements Receiver {
   }
 
   public getImportSettingMap(
-      innerFileStats: any[],
-      optionsMap: { [key: string]: GrowiArchiveImportOption; },
-      operatorUserId: string,
+    innerFileStats: any[],
+    optionsMap: { [key: string]: GrowiArchiveImportOption },
+    operatorUserId: string,
   ): Map<string, ImportSettings> {
     const importSettingsMap = new Map<string, ImportSettings>();
     innerFileStats.forEach(({ fileName, collectionName }) => {
-      const options = new GrowiArchiveImportOption(collectionName, undefined, optionsMap[collectionName]);
-
-      if (collectionName === 'configs' && options.mode !== ImportMode.flushAndInsert) {
-        throw new Error('`flushAndInsert` is only available as an import setting for configs collection');
+      const options = new GrowiArchiveImportOption(
+        collectionName,
+        undefined,
+        optionsMap[collectionName],
+      );
+
+      if (
+        collectionName === 'configs' &&
+        options.mode !== ImportMode.flushAndInsert
+      ) {
+        throw new Error(
+          '`flushAndInsert` is only available as an import setting for configs collection',
+        );
       }
       if (collectionName === 'pages' && options.mode === ImportMode.insert) {
-        throw new Error('`insert` is not available as an import setting for pages collection');
+        throw new Error(
+          '`insert` is not available as an import setting for pages collection',
+        );
       }
       if (collectionName === 'attachmentFiles.chunks') {
-        throw new Error('`attachmentFiles.chunks` must not be transferred. Please omit it from request body `collections`.');
+        throw new Error(
+          '`attachmentFiles.chunks` must not be transferred. Please omit it from request body `collections`.',
+        );
       }
       if (collectionName === 'attachmentFiles.files') {
-        throw new Error('`attachmentFiles.files` must not be transferred. Please omit it from request body `collections`.');
+        throw new Error(
+          '`attachmentFiles.files` must not be transferred. Please omit it from request body `collections`.',
+        );
       }
 
       const importSettings: ImportSettings = {
         mode: options.mode,
         jsonFileName: fileName,
-        overwriteParams: generateOverwriteParams(collectionName, operatorUserId, options),
+        overwriteParams: generateOverwriteParams(
+          collectionName,
+          operatorUserId,
+          options,
+        ),
       };
       importSettingsMap.set(collectionName, importSettings);
     });
@@ -647,14 +740,15 @@ export class G2GTransferReceiverService implements Receiver {
   }
 
   public async importCollections(
-      collections: string[],
-      importSettingsMap: Map<string, ImportSettings>,
-      sourceGROWIUploadConfigs: FileUploadConfigs,
+    collections: string[],
+    importSettingsMap: Map<string, ImportSettings>,
+    sourceGROWIUploadConfigs: FileUploadConfigs,
   ): Promise<void> {
     const { appService } = this.crowi;
     const importService = getImportService();
     /** whether to keep current file upload configs */
-    const shouldKeepUploadConfigs = configManager.getConfig('app:fileUploadType') !== 'none';
+    const shouldKeepUploadConfigs =
+      configManager.getConfig('app:fileUploadType') !== 'none';
 
     if (shouldKeepUploadConfigs) {
       /** cache file upload configs */
@@ -666,8 +760,7 @@ export class G2GTransferReceiverService implements Receiver {
       // restore file upload config from cache
       await configManager.removeConfigs(UPLOAD_CONFIG_KEYS);
       await configManager.updateConfigs(fileUploadConfigs);
-    }
-    else {
+    } else {
       // import mongo collections(overwrites file uplaod configs)
       await importService.import(collections, importSettingsMap);
 
@@ -680,25 +773,33 @@ export class G2GTransferReceiverService implements Receiver {
   }
 
   public async getFileUploadConfigs(): Promise<FileUploadConfigs> {
-    const fileUploadConfigs = Object.fromEntries(UPLOAD_CONFIG_KEYS.map((key) => {
-      return [key, configManager.getConfig(key, ConfigSource.db)];
-    })) as FileUploadConfigs;
+    const fileUploadConfigs = Object.fromEntries(
+      UPLOAD_CONFIG_KEYS.map((key) => {
+        return [key, configManager.getConfig(key, ConfigSource.db)];
+      }),
+    ) as FileUploadConfigs;
 
     return fileUploadConfigs;
   }
 
-  public async updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void> {
+  public async updateFileUploadConfigs(
+    fileUploadConfigs: FileUploadConfigs,
+  ): Promise<void> {
     const { appService } = this.crowi;
 
-    await configManager.removeConfigs(Object.keys(fileUploadConfigs) as ConfigKey[]);
+    await configManager.removeConfigs(
+      Object.keys(fileUploadConfigs) as ConfigKey[],
+    );
     await configManager.updateConfigs(fileUploadConfigs);
     await this.crowi.setUpFileUpload(true);
     await appService.setupAfterInstall();
   }
 
-  public async receiveAttachment(content: ReadStream, attachmentMap): Promise<void> {
+  public async receiveAttachment(
+    content: ReadStream,
+    attachmentMap,
+  ): Promise<void> {
     const { fileUploadService } = this.crowi;
     return fileUploadService.uploadAttachment(content, attachmentMap);
   }
-
 }

+ 18 - 17
apps/app/src/server/service/i18next.ts

@@ -1,9 +1,8 @@
-import path from 'path';
-
 import type { Lang } from '@growi/core';
-import type { InitOptions, TFunction, i18n } from 'i18next';
+import type { InitOptions, i18n, TFunction } from 'i18next';
 import { createInstance } from 'i18next';
 import resourcesToBackend from 'i18next-resources-to-backend';
+import path from 'path';
 
 import * as i18nextConfig from '^/config/i18next.config';
 
@@ -11,18 +10,20 @@ import { resolveFromRoot } from '~/server/util/project-dir-utils';
 
 import { configManager } from './config-manager';
 
+const relativePathToLocalesRoot = path.relative(
+  __dirname,
+  resolveFromRoot('public/static/locales'),
+);
 
-const relativePathToLocalesRoot = path.relative(__dirname, resolveFromRoot('public/static/locales'));
-
-const initI18next = async(overwriteOpts: InitOptions) => {
+const initI18next = async (overwriteOpts: InitOptions) => {
   const i18nInstance = createInstance();
   await i18nInstance
     .use(
-      resourcesToBackend(
-        (language: string, namespace: string) => {
-          return import(path.join(relativePathToLocalesRoot, language, `${namespace}.json`));
-        },
-      ),
+      resourcesToBackend((language: string, namespace: string) => {
+        return import(
+          path.join(relativePathToLocalesRoot, language, `${namespace}.json`)
+        );
+      }),
     )
     .init({
       ...i18nextConfig.initOptions,
@@ -32,14 +33,14 @@ const initI18next = async(overwriteOpts: InitOptions) => {
 };
 
 type Translation = {
-  t: TFunction,
-  i18n: i18n
-}
+  t: TFunction;
+  i18n: i18n;
+};
 
 type Opts = {
-  lang?: Lang,
-  ns?: string | readonly string[],
-}
+  lang?: Lang;
+  ns?: string | readonly string[];
+};
 
 export async function getTranslation(opts?: Opts): Promise<Translation> {
   const globalLang = configManager.getConfig('app:globalLang');

+ 92 - 58
apps/app/src/server/service/in-app-notification.ts

@@ -1,9 +1,7 @@
-import type {
-  HasObjectId, IUser, IPage,
-} from '@growi/core';
+import type { HasObjectId, IPage, IUser } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import { subDays } from 'date-fns/subDays';
-import type { Types, FilterQuery, UpdateQuery } from 'mongoose';
+import type { FilterQuery, Types, UpdateQuery } from 'mongoose';
 
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import { AllEssentialActions } from '~/interfaces/activity';
@@ -11,27 +9,21 @@ import type { PaginateResult } from '~/interfaces/in-app-notification';
 import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 import type { ActivityDocument } from '~/server/models/activity';
 import type { InAppNotificationDocument } from '~/server/models/in-app-notification';
-import {
-  InAppNotification,
-} from '~/server/models/in-app-notification';
+import { InAppNotification } from '~/server/models/in-app-notification';
 import InAppNotificationSettings from '~/server/models/in-app-notification-settings';
 import Subscription from '~/server/models/subscription';
 import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
-
-
 import { generateSnapshot } from './in-app-notification/in-app-notification-utils';
-import { preNotifyService, type PreNotify } from './pre-notify';
-import { RoomPrefix, getRoomNameWithId } from './socket-io/helper';
-
+import { type PreNotify, preNotifyService } from './pre-notify';
+import { getRoomNameWithId, RoomPrefix } from './socket-io/helper';
 
 const { STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
 
 const logger = loggerFactory('growi:service:inAppNotification');
 
 export default class InAppNotificationService {
-
   crowi!: Crowi;
 
   socketIoService!: any;
@@ -52,42 +44,58 @@ export default class InAppNotificationService {
   }
 
   initActivityEventListeners(): void {
-    this.activityEvent.on('updated', async(activity: ActivityDocument, target: IUser | IPage | IPageBulkExportJob, preNotify: PreNotify) => {
-      try {
-        const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
-        if (shouldNotification) {
-          await this.createInAppNotification(activity, target, preNotify);
+    this.activityEvent.on(
+      'updated',
+      async (
+        activity: ActivityDocument,
+        target: IUser | IPage | IPageBulkExportJob,
+        preNotify: PreNotify,
+      ) => {
+        try {
+          const shouldNotification =
+            activity != null &&
+            target != null &&
+            (AllEssentialActions as ReadonlyArray<string>).includes(
+              activity.action,
+            );
+          if (shouldNotification) {
+            await this.createInAppNotification(activity, target, preNotify);
+          }
+        } catch (err) {
+          logger.error('Create InAppNotification failed', err);
         }
-      }
-      catch (err) {
-        logger.error('Create InAppNotification failed', err);
-      }
-    });
+      },
+    );
   }
 
-  emitSocketIo = async(targetUsers) => {
+  emitSocketIo = async (targetUsers) => {
     if (this.socketIoService.isInitialized) {
-      targetUsers.forEach(async(userId) => {
-
+      targetUsers.forEach(async (userId) => {
         // emit to the room for each user
-        await this.socketIoService.getDefaultSocket()
+        await this.socketIoService
+          .getDefaultSocket()
           .in(getRoomNameWithId(RoomPrefix.USER, userId))
           .emit('notificationUpdated');
       });
     }
   };
 
-  upsertByActivity = async function(
-      users: Types.ObjectId[], activity: ActivityDocument, snapshot: string, createdAt?: Date | null,
-  ): Promise<void> {
-    const {
-      _id: activityId, targetModel, target, action,
-    } = activity;
+  upsertByActivity = async (
+    users: Types.ObjectId[],
+    activity: ActivityDocument,
+    snapshot: string,
+    createdAt?: Date | null,
+  ): Promise<void> => {
+    const { _id: activityId, targetModel, target, action } = activity;
     const now = createdAt || Date.now();
     const lastWeek = subDays(now, 7);
     const operations = users.map((user) => {
       const filter: FilterQuery<InAppNotificationDocument> = {
-        user, target, action, createdAt: { $gt: lastWeek }, snapshot,
+        user,
+        target,
+        action,
+        createdAt: { $gt: lastWeek },
+        snapshot,
       };
       const parameters: UpdateQuery<InAppNotificationDocument> = {
         user,
@@ -113,9 +121,13 @@ export default class InAppNotificationService {
     return;
   };
 
-  getLatestNotificationsByUser = async(
-      userId: Types.ObjectId,
-      queryOptions: {offset: number, limit: number, status?: InAppNotificationStatuses},
+  getLatestNotificationsByUser = async (
+    userId: Types.ObjectId,
+    queryOptions: {
+      offset: number;
+      limit: number;
+      status?: InAppNotificationStatuses;
+    },
   ): Promise<PaginateResult<InAppNotificationDocument>> => {
     const { limit, offset, status } = queryOptions;
 
@@ -136,9 +148,7 @@ export default class InAppNotificationService {
             { path: 'user' },
             {
               path: 'target',
-              populate: [
-                { path: 'attachment', strictPopulate: false },
-              ],
+              populate: [{ path: 'attachment', strictPopulate: false }],
             },
             { path: 'activities', populate: { path: 'user' } },
           ],
@@ -146,14 +156,16 @@ export default class InAppNotificationService {
       );
 
       return paginationResult;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Error', err);
       throw new Error(err);
     }
   };
 
-  open = async function(user: IUser & HasObjectId, id: Types.ObjectId): Promise<void> {
+  open = async (
+    user: IUser & HasObjectId,
+    id: Types.ObjectId,
+  ): Promise<void> => {
     const query = { _id: id, user: user._id };
     const parameters = { status: STATUS_OPENED };
     const options = { new: true };
@@ -162,7 +174,9 @@ export default class InAppNotificationService {
     return;
   };
 
-  updateAllNotificationsAsOpened = async function(user: IUser & HasObjectId): Promise<void> {
+  updateAllNotificationsAsOpened = async (
+    user: IUser & HasObjectId,
+  ): Promise<void> => {
     const filter = { user: user._id, status: STATUS_UNOPENED };
     const options = { status: STATUS_OPENED };
 
@@ -170,36 +184,54 @@ export default class InAppNotificationService {
     return;
   };
 
-  getUnreadCountByUser = async function(user: Types.ObjectId): Promise<number| undefined> {
+  getUnreadCountByUser = async (
+    user: Types.ObjectId,
+  ): Promise<number | undefined> => {
     const query = { user, status: STATUS_UNOPENED };
 
     try {
       const count = await InAppNotification.countDocuments(query);
 
       return count;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Error on getUnreadCountByUser', err);
       throw err;
     }
   };
 
-  createSubscription = async function(userId: Types.ObjectId, pageId: Types.ObjectId, targetRuleName: string): Promise<void> {
+  createSubscription = async (
+    userId: Types.ObjectId,
+    pageId: Types.ObjectId,
+    targetRuleName: string,
+  ): Promise<void> => {
     const query = { userId };
-    const inAppNotificationSettings = await InAppNotificationSettings.findOne(query);
+    const inAppNotificationSettings =
+      await InAppNotificationSettings.findOne(query);
     if (inAppNotificationSettings != null) {
-      const subscribeRule = inAppNotificationSettings.subscribeRules.find(subscribeRule => subscribeRule.name === targetRuleName);
+      const subscribeRule = inAppNotificationSettings.subscribeRules.find(
+        (subscribeRule) => subscribeRule.name === targetRuleName,
+      );
       if (subscribeRule != null && subscribeRule.isEnabled) {
-        await Subscription.subscribeByPageId(userId, pageId, SubscriptionStatusType.SUBSCRIBE);
+        await Subscription.subscribeByPageId(
+          userId,
+          pageId,
+          SubscriptionStatusType.SUBSCRIBE,
+        );
       }
     }
 
     return;
   };
 
-  createInAppNotification = async function(activity: ActivityDocument, target: IUser | IPage | IPageBulkExportJob, preNotify: PreNotify): Promise<void> {
-
-    const shouldNotification = activity != null && target != null && (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
+  createInAppNotification = async function (
+    activity: ActivityDocument,
+    target: IUser | IPage | IPageBulkExportJob,
+    preNotify: PreNotify,
+  ): Promise<void> {
+    const shouldNotification =
+      activity != null &&
+      target != null &&
+      (AllEssentialActions as ReadonlyArray<string>).includes(activity.action);
 
     const targetModel = activity.targetModel;
 
@@ -210,15 +242,17 @@ export default class InAppNotificationService {
 
       await preNotify(props);
 
-      await this.upsertByActivity(props.notificationTargetUsers, activity, snapshot);
+      await this.upsertByActivity(
+        props.notificationTargetUsers,
+        activity,
+        snapshot,
+      );
       await this.emitSocketIo(props.notificationTargetUsers);
-    }
-    else {
+    } else {
       throw Error('no activity to notify');
     }
     return;
   };
-
 }
 
 module.exports = InAppNotificationService;

+ 74 - 47
apps/app/src/server/service/installer.ts

@@ -1,31 +1,25 @@
-import path from 'path';
-
-import type {
-  Lang, IPage, IUser,
-} from '@growi/core';
+import type { IPage, IUser, Lang } from '@growi/core';
 import { addSeconds } from 'date-fns/addSeconds';
 import ExtensibleCustomError from 'extensible-custom-error';
 import fs from 'graceful-fs';
 import mongoose from 'mongoose';
+import path from 'path';
 
 import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
-
 import { configManager } from './config-manager';
 
 const logger = loggerFactory('growi:service:installer');
 
-export class FailedToCreateAdminUserError extends ExtensibleCustomError {
-}
+export class FailedToCreateAdminUserError extends ExtensibleCustomError {}
 
 export type AutoInstallOptions = {
-  allowGuestMode?: boolean,
-  serverDate?: Date,
-}
+  allowGuestMode?: boolean;
+  serverDate?: Date;
+};
 
 export class InstallerService {
-
   crowi: Crowi;
 
   constructor(crowi: Crowi) {
@@ -41,25 +35,26 @@ export class InstallerService {
 
     try {
       await searchService.rebuildIndex();
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Rebuild index failed', err);
     }
   }
 
-  private async createPage(filePath, pagePath): Promise<IPage|undefined> {
+  private async createPage(filePath, pagePath): Promise<IPage | undefined> {
     const { pageService } = this.crowi;
 
     try {
       const markdown = fs.readFileSync(filePath);
       return pageService.forceCreateBySystem(pagePath, markdown.toString(), {});
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(`Failed to create ${pagePath}`, err);
     }
   }
 
-  private async createInitialPages(lang: Lang, initialPagesCreatedAt?: Date): Promise<any> {
+  private async createInitialPages(
+    lang: Lang,
+    initialPagesCreatedAt?: Date,
+  ): Promise<any> {
     const { localeDir } = this.crowi;
     // create /Sandbox/*
     /*
@@ -68,10 +63,22 @@ export class InstallerService {
      *   2. avoid difference for order in VRT
      */
     await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox');
-    await this.createPage(path.join(localeDir, lang, 'sandbox-markdown.md'), '/Sandbox/Markdown');
-    await this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap5.md'), '/Sandbox/Bootstrap5');
-    await this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams');
-    await this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math');
+    await this.createPage(
+      path.join(localeDir, lang, 'sandbox-markdown.md'),
+      '/Sandbox/Markdown',
+    );
+    await this.createPage(
+      path.join(localeDir, lang, 'sandbox-bootstrap5.md'),
+      '/Sandbox/Bootstrap5',
+    );
+    await this.createPage(
+      path.join(localeDir, lang, 'sandbox-diagrams.md'),
+      '/Sandbox/Diagrams',
+    );
+    await this.createPage(
+      path.join(localeDir, lang, 'sandbox-math.md'),
+      '/Sandbox/Math',
+    );
 
     // update createdAt and updatedAt fields of all pages
     if (initialPagesCreatedAt != null) {
@@ -81,8 +88,13 @@ export class InstallerService {
         const Page = mongoose.model('Page') as any;
 
         // Increment timestamp to avoid difference for order in VRT
-        const pagePaths = ['/Sandbox', '/Sandbox/Bootstrap4', '/Sandbox/Diagrams', '/Sandbox/Math'];
-        const promises = pagePaths.map(async(path: string, idx: number) => {
+        const pagePaths = [
+          '/Sandbox',
+          '/Sandbox/Bootstrap4',
+          '/Sandbox/Diagrams',
+          '/Sandbox/Math',
+        ];
+        const promises = pagePaths.map(async (path: string, idx: number) => {
           const date = addSeconds(initialPagesCreatedAt, idx);
           return Page.update(
             { path },
@@ -93,16 +105,14 @@ export class InstallerService {
           );
         });
         await Promise.all(promises);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Failed to update createdAt', err);
       }
     }
 
     try {
       await this.initSearchIndex();
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Failed to build Elasticsearch Indices', err);
     }
   }
@@ -110,20 +120,37 @@ export class InstallerService {
   /**
    * Execute only once for installing application
    */
-  private async initDB(globalLang: Lang, options?: AutoInstallOptions): Promise<void> {
-    await configManager.updateConfigs({
-      'app:installed': true,
-      'app:fileUpload': true,
-      'app:isV5Compatible': true,
-      'app:globalLang': globalLang,
-    }, { skipPubsub: true });
+  private async initDB(
+    globalLang: Lang,
+    options?: AutoInstallOptions,
+  ): Promise<void> {
+    await configManager.updateConfigs(
+      {
+        'app:installed': true,
+        'app:fileUpload': true,
+        'app:isV5Compatible': true,
+        'app:globalLang': globalLang,
+      },
+      { skipPubsub: true },
+    );
 
     if (options?.allowGuestMode) {
-      await configManager.updateConfig('security:restrictGuestMode', 'Readonly', { skipPubsub: true });
+      await configManager.updateConfig(
+        'security:restrictGuestMode',
+        'Readonly',
+        { skipPubsub: true },
+      );
     }
   }
 
-  async install(firstAdminUserToSave: Pick<IUser, 'name' | 'username' | 'email' | 'password'>, globalLang: Lang, options?: AutoInstallOptions): Promise<IUser> {
+  async install(
+    firstAdminUserToSave: Pick<
+      IUser,
+      'name' | 'username' | 'email' | 'password'
+    >,
+    globalLang: Lang,
+    options?: AutoInstallOptions,
+  ): Promise<IUser> {
     await this.initDB(globalLang, options);
 
     const User = mongoose.model<IUser, { createUser }>('User');
@@ -134,30 +161,30 @@ export class InstallerService {
         path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
         '/',
       );
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw err;
     }
 
     try {
       // create first admin user
-      const {
-        name, username, email, password,
-      } = firstAdminUserToSave;
-      const adminUser = await User.createUser(name, username, email, password, globalLang);
+      const { name, username, email, password } = firstAdminUserToSave;
+      const adminUser = await User.createUser(
+        name,
+        username,
+        email,
+        password,
+        globalLang,
+      );
       await (adminUser as any).asyncGrantAdmin();
 
       // create initial pages
       await this.createInitialPages(globalLang, options?.serverDate);
 
       return adminUser;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new FailedToCreateAdminUserError(err);
     }
-
   }
-
 }

+ 81 - 51
apps/app/src/server/service/ldap.ts

@@ -4,25 +4,23 @@ import loggerFactory from '~/utils/logger';
 
 import { configManager } from './config-manager';
 
-
 const logger = loggerFactory('growi:service:ldap-service');
 
 // @types/ldapjs is outdated, and SearchResultEntry does not exist.
 // Declare it manually in the meantime.
 export interface SearchResultEntry {
-  objectName: string // DN
+  objectName: string; // DN
   attributes: {
-    type: string,
-    values: string | string[]
-  }[]
+    type: string;
+    values: string | string[];
+  }[];
 }
 
 /**
  * Service to connect to LDAP server.
  * User auth using LDAP is done with PassportService, not here.
-*/
+ */
 class LdapService {
-
   client: ldap.Client | null;
 
   searchBase: string;
@@ -33,7 +31,9 @@ class LdapService {
    * @param {string} userBindPassword Necessary when bind type is user bind
    */
   initClient(userBindUsername?: string, userBindPassword?: string): void {
-    const serverUrl = configManager.getConfig('security:passport-ldap:serverUrl');
+    const serverUrl = configManager.getConfig(
+      'security:passport-ldap:serverUrl',
+    );
 
     // parse serverUrl
     // see: https://regex101.com/r/0tuYBB/1
@@ -62,7 +62,9 @@ class LdapService {
     const client = this.client;
     if (client == null) throw new Error('LDAP client is not initialized');
 
-    const isLdapEnabled = configManager.getConfig('security:passport-ldap:isEnabled');
+    const isLdapEnabled = configManager.getConfig(
+      'security:passport-ldap:isEnabled',
+    );
     if (!isLdapEnabled) {
       const notEnabledMessage = 'LDAP is not enabled';
       logger.error(notEnabledMessage);
@@ -70,15 +72,21 @@ class LdapService {
     }
 
     // get configurations
-    const isUserBind = configManager.getConfig('security:passport-ldap:isUserBind');
-    const bindDN = configManager.getConfig('security:passport-ldap:bindDN') ?? '';
-    const bindCredentials = configManager.getConfig('security:passport-ldap:bindDNPassword') ?? '';
+    const isUserBind = configManager.getConfig(
+      'security:passport-ldap:isUserBind',
+    );
+    const bindDN =
+      configManager.getConfig('security:passport-ldap:bindDN') ?? '';
+    const bindCredentials =
+      configManager.getConfig('security:passport-ldap:bindDNPassword') ?? '';
 
     // user bind
-    const fixedBindDN = (isUserBind)
+    const fixedBindDN = isUserBind
       ? bindDN.replace(/{{username}}/, userBindUsername)
       : bindDN;
-    const fixedBindCredentials = (isUserBind) ? userBindPassword : bindCredentials;
+    const fixedBindCredentials = isUserBind
+      ? userBindPassword
+      : bindCredentials;
 
     return new Promise<void>((resolve, reject) => {
       client.bind(fixedBindDN, fixedBindCredentials, (err) => {
@@ -97,7 +105,11 @@ class LdapService {
    * @param {string} base Base DN to execute search on
    * @returns {SearchEntry[]} Search result. Default scope is set to 'sub'.
    */
-  search(filter?: string, base?: string, scope: 'sub' | 'base' | 'one' = 'sub'): Promise<SearchResultEntry[]> {
+  search(
+    filter?: string,
+    base?: string,
+    scope: 'sub' | 'base' | 'one' = 'sub',
+  ): Promise<SearchResultEntry[]> {
     const client = this.client;
     if (client == null) throw new Error('LDAP client is not initialized');
 
@@ -109,36 +121,43 @@ class LdapService {
         reject(err);
       });
 
-      client.search(base || this.searchBase, {
-        scope, filter, paged: true, sizeLimit: 200,
-      }, (err, res) => {
-        if (err != null) {
-          reject(err);
-        }
-
-        // @types/ldapjs is outdated, and pojo property (type SearchResultEntry) does not exist.
-        // Typecast to manually declared SearchResultEntry in the meantime.
-        res.on('searchEntry', (entry: any) => {
-          const pojo = entry?.pojo as SearchResultEntry;
-          searchResults.push(pojo);
-        });
-        res.on('error', (err) => {
-          if (err instanceof NoSuchObjectError) {
-            resolve([]);
-          }
-          else {
+      client.search(
+        base || this.searchBase,
+        {
+          scope,
+          filter,
+          paged: true,
+          sizeLimit: 200,
+        },
+        (err, res) => {
+          if (err != null) {
             reject(err);
           }
-        });
-        res.on('end', (result) => {
-          if (result?.status === 0) {
-            resolve(searchResults);
-          }
-          else {
-            reject(new Error(`LDAP search failed: status code ${result?.status}`));
-          }
-        });
-      });
+
+          // @types/ldapjs is outdated, and pojo property (type SearchResultEntry) does not exist.
+          // Typecast to manually declared SearchResultEntry in the meantime.
+          res.on('searchEntry', (entry: any) => {
+            const pojo = entry?.pojo as SearchResultEntry;
+            searchResults.push(pojo);
+          });
+          res.on('error', (err) => {
+            if (err instanceof NoSuchObjectError) {
+              resolve([]);
+            } else {
+              reject(err);
+            }
+          });
+          res.on('end', (result) => {
+            if (result?.status === 0) {
+              resolve(searchResults);
+            } else {
+              reject(
+                new Error(`LDAP search failed: status code ${result?.status}`),
+              );
+            }
+          });
+        },
+      );
     });
   }
 
@@ -146,13 +165,23 @@ class LdapService {
     return this.search(undefined, this.getGroupSearchBase());
   }
 
-  getArrayValFromSearchResultEntry(entry: SearchResultEntry, attributeType: string | undefined): string[] {
-    const values: string | string[] = entry.attributes.find(attribute => attribute.type === attributeType)?.values || [];
+  getArrayValFromSearchResultEntry(
+    entry: SearchResultEntry,
+    attributeType: string | undefined,
+  ): string[] {
+    const values: string | string[] =
+      entry.attributes.find((attribute) => attribute.type === attributeType)
+        ?.values || [];
     return typeof values === 'string' ? [values] : values;
   }
 
-  getStringValFromSearchResultEntry(entry: SearchResultEntry, attributeType: string | undefined): string | undefined {
-    const values: string | string[] | undefined = entry.attributes.find(attribute => attribute.type === attributeType)?.values;
+  getStringValFromSearchResultEntry(
+    entry: SearchResultEntry,
+    attributeType: string | undefined,
+  ): string | undefined {
+    const values: string | string[] | undefined = entry.attributes.find(
+      (attribute) => attribute.type === attributeType,
+    )?.values;
     if (typeof values === 'string' || values == null) {
       return values;
     }
@@ -163,11 +192,12 @@ class LdapService {
   }
 
   getGroupSearchBase(): string {
-    return configManager.getConfig('external-user-group:ldap:groupSearchBase')
-      ?? configManager.getConfig('security:passport-ldap:groupSearchBase')
-      ?? '';
+    return (
+      configManager.getConfig('external-user-group:ldap:groupSearchBase') ??
+      configManager.getConfig('security:passport-ldap:groupSearchBase') ??
+      ''
+    );
   }
-
 }
 
 // export the singleton instance

+ 37 - 31
apps/app/src/server/service/mail.ts

@@ -1,28 +1,24 @@
-import { promisify } from 'util';
-
 import ejs from 'ejs';
 import nodemailer from 'nodemailer';
+import { promisify } from 'util';
 
 import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
 import S2sMessage from '../models/vo/s2s-message';
-
 import type { IConfigManagerForApp } from './config-manager';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
 const logger = loggerFactory('growi:service:mail');
 
-
 type MailConfig = {
-  to?: string,
-  from?: string,
-  text?: string,
-  subject?: string,
-}
+  to?: string;
+  from?: string;
+  text?: string;
+  subject?: string;
+};
 
 class MailService implements S2sMessageHandlable {
-
   appService!: any;
 
   configManager: IConfigManagerForApp;
@@ -57,7 +53,10 @@ class MailService implements S2sMessageHandlable {
       return false;
     }
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
+    return (
+      this.lastLoadedAt == null ||
+      this.lastLoadedAt < new Date(s2sMessage.updatedAt)
+    );
   }
 
   /**
@@ -75,18 +74,21 @@ class MailService implements S2sMessageHandlable {
     const { s2sMessagingService } = this;
 
     if (s2sMessagingService != null) {
-      const s2sMessage = new S2sMessage('mailServiceUpdated', { updatedAt: new Date() });
+      const s2sMessage = new S2sMessage('mailServiceUpdated', {
+        updatedAt: new Date(),
+      });
 
       try {
         await s2sMessagingService.publish(s2sMessage);
-      }
-      catch (e) {
-        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      } catch (e) {
+        logger.error(
+          'Failed to publish update message with S2sMessagingService: ',
+          e.message,
+        );
       }
     }
   }
 
-
   initialize() {
     const { appService, configManager } = this;
 
@@ -97,15 +99,15 @@ class MailService implements S2sMessageHandlable {
       return;
     }
 
-    const transmissionMethod = configManager.getConfig('mail:transmissionMethod');
+    const transmissionMethod = configManager.getConfig(
+      'mail:transmissionMethod',
+    );
 
     if (transmissionMethod === 'smtp') {
       this.mailer = this.createSMTPClient();
-    }
-    else if (transmissionMethod === 'ses') {
+    } else if (transmissionMethod === 'ses') {
       this.mailer = this.createSESClient();
-    }
-    else {
+    } else {
       this.mailer = null;
     }
 
@@ -130,7 +132,8 @@ class MailService implements S2sMessageHandlable {
       if (host == null || port == null) {
         return null;
       }
-      option = { // eslint-disable-line no-param-reassign
+      option = {
+        // eslint-disable-line no-param-reassign
         host,
         port,
       };
@@ -159,11 +162,14 @@ class MailService implements S2sMessageHandlable {
 
     if (!option) {
       const accessKeyId = configManager.getConfig('mail:sesAccessKeyId');
-      const secretAccessKey = configManager.getConfig('mail:sesSecretAccessKey');
+      const secretAccessKey = configManager.getConfig(
+        'mail:sesSecretAccessKey',
+      );
       if (accessKeyId == null || secretAccessKey == null) {
         return null;
       }
-      option = { // eslint-disable-line no-param-reassign
+      option = {
+        // eslint-disable-line no-param-reassign
         accessKeyId,
         secretAccessKey,
       };
@@ -193,21 +199,21 @@ class MailService implements S2sMessageHandlable {
 
   async send(config) {
     if (this.mailer == null) {
-      throw new Error('Mailer is not completed to set up. Please set up SMTP or AWS setting.');
+      throw new Error(
+        'Mailer is not completed to set up. Please set up SMTP or AWS setting.',
+      );
     }
 
-    const renderFilePromisified = promisify<string, ejs.Data, string>(ejs.renderFile);
+    const renderFilePromisified = promisify<string, ejs.Data, string>(
+      ejs.renderFile,
+    );
 
     const templateVars = config.vars || {};
-    const output = await renderFilePromisified(
-      config.template,
-      templateVars,
-    );
+    const output = await renderFilePromisified(config.template, templateVars);
 
     config.text = output;
     return this.mailer.sendMail(this.setupMailConfig(config));
   }
-
 }
 
 module.exports = MailService;

File diff ditekan karena terlalu besar
+ 513 - 206
apps/app/src/server/service/page-grant.ts


+ 97 - 40
apps/app/src/server/service/page-operation.ts

@@ -2,8 +2,11 @@ import type { IPage } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import mongoose from 'mongoose';
 
-import type { IPageOperationProcessInfo, IPageOperationProcessData } from '~/interfaces/page-operation';
-import { PageActionType, PageActionStage } from '~/interfaces/page-operation';
+import type {
+  IPageOperationProcessData,
+  IPageOperationProcessInfo,
+} from '~/interfaces/page-operation';
+import { PageActionStage, PageActionType } from '~/interfaces/page-operation';
 import type { PageOperationDocument } from '~/server/models/page-operation';
 import PageOperation from '~/server/models/page-operation';
 import loggerFactory from '~/utils/logger';
@@ -15,26 +18,35 @@ import { collectAncestorPaths } from '../util/collect-ancestor-paths';
 
 const logger = loggerFactory('growi:services:page-operation');
 
-const {
-  isEitherOfPathAreaOverlap, isPathAreaOverlap, isTrashPage,
-} = pagePathUtils;
+const { isEitherOfPathAreaOverlap, isPathAreaOverlap, isTrashPage } =
+  pagePathUtils;
 const AUTO_UPDATE_INTERVAL_SEC = 5;
 
 const {
-  Create, Update,
-  Duplicate, Delete, DeleteCompletely, Revert, NormalizeParent,
+  Create,
+  Update,
+  Duplicate,
+  Delete,
+  DeleteCompletely,
+  Revert,
+  NormalizeParent,
 } = PageActionType;
 
 export interface IPageOperationService {
-  generateProcessInfo(pageOperations: PageOperationDocument[]): IPageOperationProcessInfo;
-  canOperate(isRecursively: boolean, fromPathToOp: string | null, toPathToOp: string | null): Promise<boolean>;
+  generateProcessInfo(
+    pageOperations: PageOperationDocument[],
+  ): IPageOperationProcessInfo;
+  canOperate(
+    isRecursively: boolean,
+    fromPathToOp: string | null,
+    toPathToOp: string | null,
+  ): Promise<boolean>;
   autoUpdateExpiryDate(operationId: ObjectIdLike): NodeJS.Timeout;
   clearAutoUpdateInterval(timerObj: NodeJS.Timeout): void;
   getAncestorsPathsByFromAndToPath(fromPath: string, toPath: string): string[];
 }
 
 class PageOperationService implements IPageOperationService {
-
   crowi: Crowi;
 
   constructor(crowi: Crowi) {
@@ -43,9 +55,20 @@ class PageOperationService implements IPageOperationService {
 
   async init(): Promise<void> {
     // cleanup PageOperation documents except ones with { actionType: Rename, actionStage: Sub }
-    const types = [Create, Update, Duplicate, Delete, DeleteCompletely, Revert, NormalizeParent];
+    const types = [
+      Create,
+      Update,
+      Duplicate,
+      Delete,
+      DeleteCompletely,
+      Revert,
+      NormalizeParent,
+    ];
     await PageOperation.deleteByActionTypes(types);
-    await PageOperation.deleteMany({ actionType: PageActionType.Rename, actionStage: PageActionStage.Main });
+    await PageOperation.deleteMany({
+      actionType: PageActionType.Rename,
+      actionStage: PageActionStage.Main,
+    });
   }
 
   /**
@@ -53,12 +76,13 @@ class PageOperationService implements IPageOperationService {
    */
   async afterExpressServerReady(): Promise<void> {
     try {
-      const pageOps = await PageOperation.find({ actionType: PageActionType.Rename, actionStage: PageActionStage.Sub })
-        .sort({ createdAt: 'asc' });
+      const pageOps = await PageOperation.find({
+        actionType: PageActionType.Rename,
+        actionStage: PageActionStage.Sub,
+      }).sort({ createdAt: 'asc' });
       // execute rename operation
       await this.executeAllRenameOperationBySystem(pageOps);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
     }
   }
@@ -66,13 +90,14 @@ class PageOperationService implements IPageOperationService {
   /**
    * Execute renameSubOperation on every page operation for rename ordered by createdAt ASC
    */
-  private async executeAllRenameOperationBySystem(pageOps: PageOperationDocument[]): Promise<void> {
+  private async executeAllRenameOperationBySystem(
+    pageOps: PageOperationDocument[],
+  ): Promise<void> {
     if (pageOps.length === 0) return;
 
     const Page = mongoose.model<IPage, PageModel>('Page');
 
     for await (const pageOp of pageOps) {
-
       const renamedPage = await Page.findById(pageOp.page._id);
       if (renamedPage == null) {
         logger.warn('operating page is not found');
@@ -80,7 +105,10 @@ class PageOperationService implements IPageOperationService {
       }
 
       // rename
-      await this.crowi.pageService.resumeRenameSubOperation(renamedPage, pageOp);
+      await this.crowi.pageService.resumeRenameSubOperation(
+        renamedPage,
+        pageOp,
+      );
     }
   }
 
@@ -91,48 +119,64 @@ class PageOperationService implements IPageOperationService {
    * @param toPathToOp The path to operate to
    * @returns boolean
    */
-  async canOperate(isRecursively: boolean, fromPathToOp: string | null, toPathToOp: string | null): Promise<boolean> {
+  async canOperate(
+    isRecursively: boolean,
+    fromPathToOp: string | null,
+    toPathToOp: string | null,
+  ): Promise<boolean> {
     const pageOperations = await PageOperation.find();
 
     if (pageOperations.length === 0) {
       return true;
     }
 
-    const fromPaths = pageOperations.map(op => op.fromPath).filter((p): p is string => p != null);
-    const toPaths = pageOperations.map(op => op.toPath).filter((p): p is string => p != null);
+    const fromPaths = pageOperations
+      .map((op) => op.fromPath)
+      .filter((p): p is string => p != null);
+    const toPaths = pageOperations
+      .map((op) => op.toPath)
+      .filter((p): p is string => p != null);
 
     if (isRecursively) {
       if (fromPathToOp != null && !isTrashPage(fromPathToOp)) {
-        const fromFlag = fromPaths.some(p => isEitherOfPathAreaOverlap(p, fromPathToOp));
+        const fromFlag = fromPaths.some((p) =>
+          isEitherOfPathAreaOverlap(p, fromPathToOp),
+        );
         if (fromFlag) return false;
 
-        const toFlag = toPaths.some(p => isEitherOfPathAreaOverlap(p, fromPathToOp));
+        const toFlag = toPaths.some((p) =>
+          isEitherOfPathAreaOverlap(p, fromPathToOp),
+        );
         if (toFlag) return false;
       }
 
       if (toPathToOp != null && !isTrashPage(toPathToOp)) {
-        const fromFlag = fromPaths.some(p => isPathAreaOverlap(p, toPathToOp));
+        const fromFlag = fromPaths.some((p) =>
+          isPathAreaOverlap(p, toPathToOp),
+        );
         if (fromFlag) return false;
 
-        const toFlag = toPaths.some(p => isPathAreaOverlap(p, toPathToOp));
+        const toFlag = toPaths.some((p) => isPathAreaOverlap(p, toPathToOp));
         if (toFlag) return false;
       }
-
-    }
-    else {
+    } else {
       if (fromPathToOp != null && !isTrashPage(fromPathToOp)) {
-        const fromFlag = fromPaths.some(p => isPathAreaOverlap(p, fromPathToOp));
+        const fromFlag = fromPaths.some((p) =>
+          isPathAreaOverlap(p, fromPathToOp),
+        );
         if (fromFlag) return false;
 
-        const toFlag = toPaths.some(p => isPathAreaOverlap(p, fromPathToOp));
+        const toFlag = toPaths.some((p) => isPathAreaOverlap(p, fromPathToOp));
         if (toFlag) return false;
       }
 
       if (toPathToOp != null && !isTrashPage(toPathToOp)) {
-        const fromFlag = fromPaths.some(p => isPathAreaOverlap(p, toPathToOp));
+        const fromFlag = fromPaths.some((p) =>
+          isPathAreaOverlap(p, toPathToOp),
+        );
         if (fromFlag) return false;
 
-        const toFlag = toPaths.some(p => isPathAreaOverlap(p, toPathToOp));
+        const toFlag = toPaths.some((p) => isPathAreaOverlap(p, toPathToOp));
         if (toFlag) return false;
       }
     }
@@ -144,7 +188,9 @@ class PageOperationService implements IPageOperationService {
    * Generate object that connects page id with processData of PageOperation.
    * The processData is a combination of actionType as a key and information on whether the action is processable as a value.
    */
-  generateProcessInfo(pageOps: PageOperationDocument[]): IPageOperationProcessInfo {
+  generateProcessInfo(
+    pageOps: PageOperationDocument[],
+  ): IPageOperationProcessInfo {
     const processInfo: IPageOperationProcessInfo = {};
 
     pageOps.forEach((pageOp) => {
@@ -154,8 +200,14 @@ class PageOperationService implements IPageOperationService {
       const isProcessable = pageOp.isProcessable();
 
       // processData for processInfo
-      const mainProcessableInfo = pageOp.actionStage === PageActionStage.Main ? { isProcessable } : undefined;
-      const subProcessableInfo = pageOp.actionStage === PageActionStage.Sub ? { isProcessable } : undefined;
+      const mainProcessableInfo =
+        pageOp.actionStage === PageActionStage.Main
+          ? { isProcessable }
+          : undefined;
+      const subProcessableInfo =
+        pageOp.actionStage === PageActionStage.Sub
+          ? { isProcessable }
+          : undefined;
       const processData: IPageOperationProcessData = {
         [actionType]: {
           [PageActionStage.Main]: mainProcessableInfo,
@@ -182,7 +234,7 @@ class PageOperationService implements IPageOperationService {
    */
   autoUpdateExpiryDate(operationId: ObjectIdLike): NodeJS.Timeout {
     // https://github.com/Microsoft/TypeScript/issues/30128#issuecomment-651877225
-    const timerObj = global.setInterval(async() => {
+    const timerObj = global.setInterval(async () => {
       await PageOperation.extendExpiryDate(operationId);
     }, AUTO_UPDATE_INTERVAL_SEC * 1000);
     return timerObj;
@@ -202,11 +254,16 @@ class PageOperationService implements IPageOperationService {
     return Array.from(new Set(toAncestorsPaths.concat(fromAncestorsPaths)));
   }
 
-  async getRenameSubOperationByPageId(pageId: ObjectIdLike): Promise<PageOperationDocument | null> {
-    const filter = { actionType: PageActionType.Rename, actionStage: PageActionStage.Sub, 'page._id': pageId };
+  async getRenameSubOperationByPageId(
+    pageId: ObjectIdLike,
+  ): Promise<PageOperationDocument | null> {
+    const filter = {
+      actionType: PageActionType.Rename,
+      actionStage: PageActionStage.Sub,
+      'page._id': pageId,
+    };
     return PageOperation.findOne(filter);
   }
-
 }
 
 // eslint-disable-next-line import/no-mutable-exports

+ 59 - 55
apps/app/src/server/service/passport.spec.ts

@@ -8,10 +8,9 @@ import { configManager } from './config-manager';
 import PassportService from './passport';
 
 describe('PassportService test', () => {
-
   let crowiMock;
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     crowiMock = mock<Crowi>({
       event: vi.fn().mockImplementation((eventName) => {
         if (eventName === 'user') {
@@ -24,71 +23,76 @@ describe('PassportService test', () => {
   });
 
   describe('verifySAMLResponseByABLCRule()', () => {
-
     const passportService = new PassportService(crowiMock);
 
     let getConfigSpy: MockInstance<typeof configManager.getConfig>;
-    let extractAttributesFromSAMLResponseSpy: MockInstance<typeof passportService.extractAttributesFromSAMLResponse>;
+    let extractAttributesFromSAMLResponseSpy: MockInstance<
+      typeof passportService.extractAttributesFromSAMLResponse
+    >;
 
-    beforeEach(async() => {
+    beforeEach(async () => {
       // prepare spy for ConfigManager.getConfig
       getConfigSpy = vi.spyOn(configManager, 'getConfig');
       // prepare spy for extractAttributesFromSAMLResponse method
-      extractAttributesFromSAMLResponseSpy = vi.spyOn(passportService, 'extractAttributesFromSAMLResponse');
+      extractAttributesFromSAMLResponseSpy = vi.spyOn(
+        passportService,
+        'extractAttributesFromSAMLResponse',
+      );
     });
 
     /* eslint-disable indent */
     let i = 0;
     describe.each`
-      conditionId | departments   | positions     | ruleStr                                                         | expected
-      ${i++}      | ${undefined}  | ${undefined}  | ${' '}                                                          | ${true}
-      ${i++}      | ${undefined}  | ${undefined}  | ${'Department: A'}                                              | ${false}
-      ${i++}      | ${[]}         | ${['Leader']} | ${'Position'}                                                   | ${true}
-      ${i++}      | ${[]}         | ${['Leader']} | ${'Position: Leader'}                                           | ${true}
-      ${i++}      | ${['A']}      | ${[]}         | ${'Department: A || Department: B && Position: Leader'}         | ${true}
-      ${i++}      | ${['B']}      | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${true}
-      ${i++}      | ${['A', 'C']} | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${true}
-      ${i++}      | ${['B', 'C']} | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${true}
-      ${i++}      | ${[]}         | ${[]}         | ${'Department: A || Department: B && Position: Leader'}         | ${false}
-      ${i++}      | ${['C']}      | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}         | ${false}
-      ${i++}      | ${['A']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'}       | ${true}
-      ${i++}      | ${['B']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'}       | ${true}
-      ${i++}      | ${['C']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'}       | ${false}
-      ${i++}      | ${['A', 'B']} | ${[]}         | ${'(Department: A || Department: B) && Position: Leader'}       | ${false}
-      ${i++}      | ${['A']}      | ${[]}         | ${'Department: A NOT Position: Leader'}                         | ${true}
-      ${i++}      | ${['A']}      | ${['Leader']} | ${'Department: A NOT Position: Leader'}                         | ${false}
-      ${i++}      | ${[]}         | ${['Leader']} | ${'Department: A OR (Position NOT Position: User)'}             | ${true}
-      ${i++}      | ${[]}         | ${['User']}   | ${'Department: A OR (Position NOT Position: User)'}             | ${false}
-    `('to be $expected under rule="$ruleStr"', ({
-      conditionId, departments, positions, ruleStr, expected,
-    }) => {
-      test(`when conditionId=${conditionId}`, async() => {
-        const responseMock = {};
-
-        // setup mock implementation
-        getConfigSpy.mockImplementation((key) => {
-          if (key === 'security:passport-saml:ABLCRule') {
-            return ruleStr;
-          }
-          throw new Error('Unexpected behavior.');
-        });
-        extractAttributesFromSAMLResponseSpy.mockImplementation((response) => {
-          if (response !== responseMock) {
-            throw new Error('Unexpected args.');
-          }
-          return {
-            Department: departments,
-            Position: positions,
-          };
+      conditionId | departments   | positions     | ruleStr                                                   | expected
+      ${i++}      | ${undefined}  | ${undefined}  | ${' '}                                                    | ${true}
+      ${i++}      | ${undefined}  | ${undefined}  | ${'Department: A'}                                        | ${false}
+      ${i++}      | ${[]}         | ${['Leader']} | ${'Position'}                                             | ${true}
+      ${i++}      | ${[]}         | ${['Leader']} | ${'Position: Leader'}                                     | ${true}
+      ${i++}      | ${['A']}      | ${[]}         | ${'Department: A || Department: B && Position: Leader'}   | ${true}
+      ${i++}      | ${['B']}      | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}   | ${true}
+      ${i++}      | ${['A', 'C']} | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}   | ${true}
+      ${i++}      | ${['B', 'C']} | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}   | ${true}
+      ${i++}      | ${[]}         | ${[]}         | ${'Department: A || Department: B && Position: Leader'}   | ${false}
+      ${i++}      | ${['C']}      | ${['Leader']} | ${'Department: A || Department: B && Position: Leader'}   | ${false}
+      ${i++}      | ${['A']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'} | ${true}
+      ${i++}      | ${['B']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'} | ${true}
+      ${i++}      | ${['C']}      | ${['Leader']} | ${'(Department: A || Department: B) && Position: Leader'} | ${false}
+      ${i++}      | ${['A', 'B']} | ${[]}         | ${'(Department: A || Department: B) && Position: Leader'} | ${false}
+      ${i++}      | ${['A']}      | ${[]}         | ${'Department: A NOT Position: Leader'}                   | ${true}
+      ${i++}      | ${['A']}      | ${['Leader']} | ${'Department: A NOT Position: Leader'}                   | ${false}
+      ${i++}      | ${[]}         | ${['Leader']} | ${'Department: A OR (Position NOT Position: User)'}       | ${true}
+      ${i++}      | ${[]}         | ${['User']}   | ${'Department: A OR (Position NOT Position: User)'}       | ${false}
+    `(
+      'to be $expected under rule="$ruleStr"',
+      ({ conditionId, departments, positions, ruleStr, expected }) => {
+        test(`when conditionId=${conditionId}`, async () => {
+          const responseMock = {};
+
+          // setup mock implementation
+          getConfigSpy.mockImplementation((key) => {
+            if (key === 'security:passport-saml:ABLCRule') {
+              return ruleStr;
+            }
+            throw new Error('Unexpected behavior.');
+          });
+          extractAttributesFromSAMLResponseSpy.mockImplementation(
+            (response) => {
+              if (response !== responseMock) {
+                throw new Error('Unexpected args.');
+              }
+              return {
+                Department: departments,
+                Position: positions,
+              };
+            },
+          );
+
+          const result =
+            passportService.verifySAMLResponseByABLCRule(responseMock);
+
+          expect(result).toBe(expected);
         });
-
-        const result = passportService.verifySAMLResponseByABLCRule(responseMock);
-
-        expect(result).toBe(expected);
-      });
-    });
-
+      },
+    );
   });
-
-
 });

+ 341 - 171
apps/app/src/server/service/passport.ts

@@ -1,8 +1,11 @@
-import type { IncomingMessage } from 'http';
-
 import axiosRetry from 'axios-retry';
+import type { IncomingMessage } from 'http';
 import luceneQueryParser from 'lucene-query-parser';
-import { Strategy as OidcStrategy, Issuer as OIDCIssuer, custom } from 'openid-client';
+import {
+  custom,
+  Issuer as OIDCIssuer,
+  Strategy as OidcStrategy,
+} from 'openid-client';
 import pRetry from 'p-retry';
 import passport from 'passport';
 import { Strategy as GitHubStrategy } from 'passport-github';
@@ -17,7 +20,6 @@ import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provi
 import loggerFactory from '~/utils/logger';
 
 import S2sMessage from '../models/vo/s2s-message';
-
 import { configManager } from './config-manager';
 import type { ConfigKey } from './config-manager/config-definition';
 import { growiInfoService } from './growi-info';
@@ -25,7 +27,6 @@ import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
 const logger = loggerFactory('growi:service:PassportService');
 
-
 interface IncomingMessageWithLdapAccountInfo extends IncomingMessage {
   ldapAccountInfo: any;
 }
@@ -34,11 +35,14 @@ interface IncomingMessageWithLdapAccountInfo extends IncomingMessage {
  * the service class of Passport
  */
 class PassportService implements S2sMessageHandlable {
-
   // see '/lib/form/login.js'
-  static get USERNAME_FIELD() { return 'loginForm[username]' }
+  static get USERNAME_FIELD() {
+    return 'loginForm[username]';
+  }
 
-  static get PASSWORD_FIELD() { return 'loginForm[password]' }
+  static get PASSWORD_FIELD() {
+    return 'loginForm[password]';
+  }
 
   crowi!: any;
 
@@ -122,17 +126,23 @@ class PassportService implements S2sMessageHandlable {
     this.crowi = crowi;
   }
 
-
   /**
    * @inheritdoc
    */
   shouldHandleS2sMessage(s2sMessage) {
     const { eventName, updatedAt, strategyId } = s2sMessage;
-    if (eventName !== 'passportServiceUpdated' || updatedAt == null || strategyId == null) {
+    if (
+      eventName !== 'passportServiceUpdated' ||
+      updatedAt == null ||
+      strategyId == null
+    ) {
       return false;
     }
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
+    return (
+      this.lastLoadedAt == null ||
+      this.lastLoadedAt < new Date(s2sMessage.updatedAt)
+    );
   }
 
   /**
@@ -158,9 +168,11 @@ class PassportService implements S2sMessageHandlable {
 
       try {
         await s2sMessagingService.publish(s2sMessage);
-      }
-      catch (e) {
-        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      } catch (e) {
+        logger.error(
+          'Failed to publish update message with S2sMessagingService: ',
+          e.message,
+        );
       }
     }
   }
@@ -174,12 +186,24 @@ class PassportService implements S2sMessageHandlable {
   getSetupStrategies() {
     const setupStrategies: string[] = [];
 
-    if (this.isLocalStrategySetup) { setupStrategies.push('local') }
-    if (this.isLdapStrategySetup) { setupStrategies.push('ldap') }
-    if (this.isSamlStrategySetup) { setupStrategies.push('saml') }
-    if (this.isOidcStrategySetup) { setupStrategies.push('oidc') }
-    if (this.isGoogleStrategySetup) { setupStrategies.push('google') }
-    if (this.isGitHubStrategySetup) { setupStrategies.push('github') }
+    if (this.isLocalStrategySetup) {
+      setupStrategies.push('local');
+    }
+    if (this.isLdapStrategySetup) {
+      setupStrategies.push('ldap');
+    }
+    if (this.isSamlStrategySetup) {
+      setupStrategies.push('saml');
+    }
+    if (this.isOidcStrategySetup) {
+      setupStrategies.push('oidc');
+    }
+    if (this.isGoogleStrategySetup) {
+      setupStrategies.push('google');
+    }
+    if (this.isGitHubStrategySetup) {
+      setupStrategies.push('github');
+    }
 
     return setupStrategies;
   }
@@ -202,8 +226,7 @@ class PassportService implements S2sMessageHandlable {
 
     try {
       await this[func.setup]();
-    }
-    catch (err) {
+    } catch (err) {
       logger.debug(err);
       this[func.reset]();
     }
@@ -228,12 +251,13 @@ class PassportService implements S2sMessageHandlable {
    * @memberof PassportService
    */
   setupLocalStrategy() {
-
     this.resetLocalStrategy();
 
     const { configManager } = this.crowi;
 
-    const isEnabled = configManager.getConfig('security:passport-local:isEnabled');
+    const isEnabled = configManager.getConfig(
+      'security:passport-local:isEnabled',
+    );
 
     // when disabled
     if (!isEnabled) {
@@ -244,23 +268,27 @@ class PassportService implements S2sMessageHandlable {
 
     const User = this.crowi.model('User');
 
-    passport.use(new LocalStrategy(
-      {
-        usernameField: PassportService.USERNAME_FIELD,
-        passwordField: PassportService.PASSWORD_FIELD,
-      },
-      (username, password, done) => {
-        // find user
-        User.findUserByUsernameOrEmail(username, password, (err, user) => {
-          if (err) { return done(err) }
-          // check existence and password
-          if (!user || !user.isPasswordValid(password)) {
-            return done(null, false, { message: 'Incorrect credentials.' });
-          }
-          return done(null, user);
-        });
-      },
-    ));
+    passport.use(
+      new LocalStrategy(
+        {
+          usernameField: PassportService.USERNAME_FIELD,
+          passwordField: PassportService.PASSWORD_FIELD,
+        },
+        (username, password, done) => {
+          // find user
+          User.findUserByUsernameOrEmail(username, password, (err, user) => {
+            if (err) {
+              return done(err);
+            }
+            // check existence and password
+            if (!user || !user.isPasswordValid(password)) {
+              return done(null, false, { message: 'Incorrect credentials.' });
+            }
+            return done(null, user);
+          });
+        },
+      ),
+    );
 
     this.isLocalStrategySetup = true;
     logger.debug('LocalStrategy: setup is done');
@@ -283,13 +311,14 @@ class PassportService implements S2sMessageHandlable {
    * @memberof PassportService
    */
   setupLdapStrategy() {
-
     this.resetLdapStrategy();
 
     const config = this.crowi.config;
     const { configManager } = this.crowi;
 
-    const isLdapEnabled = configManager.getConfig('security:passport-ldap:isEnabled');
+    const isLdapEnabled = configManager.getConfig(
+      'security:passport-ldap:isEnabled',
+    );
 
     // when disabled
     if (!isLdapEnabled) {
@@ -298,15 +327,20 @@ class PassportService implements S2sMessageHandlable {
 
     logger.debug('LdapStrategy: setting up..');
 
-    passport.use(new LdapStrategy(this.getLdapConfigurationFunc(config, { passReqToCallback: true }),
-      (req, ldapAccountInfo, done) => {
-        logger.debug('LDAP authentication has succeeded', ldapAccountInfo);
+    passport.use(
+      new LdapStrategy(
+        this.getLdapConfigurationFunc(config, { passReqToCallback: true }),
+        (req, ldapAccountInfo, done) => {
+          logger.debug('LDAP authentication has succeeded', ldapAccountInfo);
 
-        // store ldapAccountInfo to req
-        (req as IncomingMessageWithLdapAccountInfo).ldapAccountInfo = ldapAccountInfo;
+          // store ldapAccountInfo to req
+          (req as IncomingMessageWithLdapAccountInfo).ldapAccountInfo =
+            ldapAccountInfo;
 
-        done(null, ldapAccountInfo);
-      }));
+          done(null, ldapAccountInfo);
+        },
+      ),
+    );
 
     this.isLdapStrategySetup = true;
     logger.debug('LdapStrategy: setup is done');
@@ -319,7 +353,9 @@ class PassportService implements S2sMessageHandlable {
    * @memberof PassportService
    */
   getLdapAttrNameMappedToUsername() {
-    return configManager.getConfig('security:passport-ldap:attrMapUsername') || 'uid';
+    return (
+      configManager.getConfig('security:passport-ldap:attrMapUsername') || 'uid'
+    );
   }
 
   /**
@@ -339,7 +375,9 @@ class PassportService implements S2sMessageHandlable {
    * @memberof PassportService
    */
   getLdapAttrNameMappedToMail() {
-    return configManager.getConfig('security:passport-ldap:attrMapMail') || 'mail';
+    return (
+      configManager.getConfig('security:passport-ldap:attrMapMail') || 'mail'
+    );
   }
 
   /**
@@ -367,14 +405,28 @@ class PassportService implements S2sMessageHandlable {
     const { configManager } = this.crowi;
 
     // get configurations
-    const isUserBind        = configManager.getConfig('security:passport-ldap:isUserBind');
-    const serverUrl         = configManager.getConfig('security:passport-ldap:serverUrl');
-    const bindDN            = configManager.getConfig('security:passport-ldap:bindDN');
-    const bindCredentials   = configManager.getConfig('security:passport-ldap:bindDNPassword');
-    const searchFilter      = configManager.getConfig('security:passport-ldap:searchFilter') || '(uid={{username}})';
-    const groupSearchBase   = configManager.getConfig('security:passport-ldap:groupSearchBase');
-    const groupSearchFilter = configManager.getConfig('security:passport-ldap:groupSearchFilter');
-    const groupDnProperty   = configManager.getConfig('security:passport-ldap:groupDnProperty') || 'uid';
+    const isUserBind = configManager.getConfig(
+      'security:passport-ldap:isUserBind',
+    );
+    const serverUrl = configManager.getConfig(
+      'security:passport-ldap:serverUrl',
+    );
+    const bindDN = configManager.getConfig('security:passport-ldap:bindDN');
+    const bindCredentials = configManager.getConfig(
+      'security:passport-ldap:bindDNPassword',
+    );
+    const searchFilter =
+      configManager.getConfig('security:passport-ldap:searchFilter') ||
+      '(uid={{username}})';
+    const groupSearchBase = configManager.getConfig(
+      'security:passport-ldap:groupSearchBase',
+    );
+    const groupSearchFilter = configManager.getConfig(
+      'security:passport-ldap:groupSearchFilter',
+    );
+    const groupDnProperty =
+      configManager.getConfig('security:passport-ldap:groupDnProperty') ||
+      'uid';
     /* eslint-enable no-multi-spaces */
 
     // parse serverUrl
@@ -382,7 +434,9 @@ class PassportService implements S2sMessageHandlable {
     const match = serverUrl.match(/(ldaps?:\/\/[^/]+)\/(.*)?/);
     if (match == null || match.length < 1) {
       logger.debug('LdapStrategy: serverUrl is invalid');
-      return (req, callback) => { callback({ message: 'serverUrl is invalid' }) };
+      return (req, callback) => {
+        callback({ message: 'serverUrl is invalid' });
+      };
     }
     const url = match[1];
     const searchBase = match[2] || '';
@@ -407,10 +461,12 @@ class PassportService implements S2sMessageHandlable {
       }
 
       // user bind
-      const fixedBindDN = (isUserBind)
+      const fixedBindDN = isUserBind
         ? bindDN.replace(/{{username}}/, loginForm.username)
         : bindDN;
-      const fixedBindCredentials = (isUserBind) ? loginForm.password : bindCredentials;
+      const fixedBindCredentials = isUserBind
+        ? loginForm.password
+        : bindCredentials;
       let serverOpt = {
         url,
         bindDN: fixedBindDN,
@@ -422,15 +478,22 @@ class PassportService implements S2sMessageHandlable {
       };
 
       if (groupSearchBase && groupSearchFilter) {
-        serverOpt = Object.assign(serverOpt, { groupSearchBase, groupSearchFilter, groupDnProperty });
+        serverOpt = Object.assign(serverOpt, {
+          groupSearchBase,
+          groupSearchFilter,
+          groupDnProperty,
+        });
       }
 
       process.nextTick(() => {
-        const mergedOpts = Object.assign({
-          usernameField: PassportService.USERNAME_FIELD,
-          passwordField: PassportService.PASSWORD_FIELD,
-          server: serverOpt,
-        }, opts);
+        const mergedOpts = Object.assign(
+          {
+            usernameField: PassportService.USERNAME_FIELD,
+            passwordField: PassportService.PASSWORD_FIELD,
+            server: serverOpt,
+          },
+          opts,
+        );
         logger.debug('ldap configuration: ', mergedOpts);
 
         // store configuration to req
@@ -447,10 +510,11 @@ class PassportService implements S2sMessageHandlable {
    * @memberof PassportService
    */
   setupGoogleStrategy() {
-
     this.resetGoogleStrategy();
 
-    const isGoogleEnabled = configManager.getConfig('security:passport-google:isEnabled');
+    const isGoogleEnabled = configManager.getConfig(
+      'security:passport-google:isEnabled',
+    );
 
     // when disabled
     if (!isGoogleEnabled) {
@@ -461,11 +525,21 @@ class PassportService implements S2sMessageHandlable {
     passport.use(
       new GoogleStrategy(
         {
-          clientID: configManager.getConfig('security:passport-google:clientId'),
-          clientSecret: configManager.getConfig('security:passport-google:clientSecret'),
-          callbackURL: configManager.getConfig('app:siteUrl') != null
-            ? urljoin(growiInfoService.getSiteUrl(), '/passport/google/callback') // auto-generated with v3.2.4 and above
-            : configManager.getConfigLegacy<string>('security:passport-google:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
+          clientID: configManager.getConfig(
+            'security:passport-google:clientId',
+          ),
+          clientSecret: configManager.getConfig(
+            'security:passport-google:clientSecret',
+          ),
+          callbackURL:
+            configManager.getConfig('app:siteUrl') != null
+              ? urljoin(
+                  growiInfoService.getSiteUrl(),
+                  '/passport/google/callback',
+                ) // auto-generated with v3.2.4 and above
+              : configManager.getConfigLegacy<string>(
+                  'security:passport-google:callbackUrl',
+                ), // DEPRECATED: backward compatible with v3.2.3 and below
           skipUserProfile: false,
         },
         (accessToken, refreshToken, profile, done) => {
@@ -494,10 +568,11 @@ class PassportService implements S2sMessageHandlable {
   }
 
   setupGitHubStrategy() {
-
     this.resetGitHubStrategy();
 
-    const isGitHubEnabled = configManager.getConfig('security:passport-github:isEnabled');
+    const isGitHubEnabled = configManager.getConfig(
+      'security:passport-github:isEnabled',
+    );
 
     // when disabled
     if (!isGitHubEnabled) {
@@ -508,11 +583,21 @@ class PassportService implements S2sMessageHandlable {
     passport.use(
       new GitHubStrategy(
         {
-          clientID: configManager.getConfig('security:passport-github:clientId'),
-          clientSecret: configManager.getConfig('security:passport-github:clientSecret'),
-          callbackURL: configManager.getConfig('app:siteUrl') != null
-            ? urljoin(growiInfoService.getSiteUrl(), '/passport/github/callback') // auto-generated with v3.2.4 and above
-            : configManager.getConfigLegacy('security:passport-github:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
+          clientID: configManager.getConfig(
+            'security:passport-github:clientId',
+          ),
+          clientSecret: configManager.getConfig(
+            'security:passport-github:clientSecret',
+          ),
+          callbackURL:
+            configManager.getConfig('app:siteUrl') != null
+              ? urljoin(
+                  growiInfoService.getSiteUrl(),
+                  '/passport/github/callback',
+                ) // auto-generated with v3.2.4 and above
+              : configManager.getConfigLegacy(
+                  'security:passport-github:callbackUrl',
+                ), // DEPRECATED: backward compatible with v3.2.3 and below
           skipUserProfile: false,
         },
         (accessToken, refreshToken, profile, done) => {
@@ -541,10 +626,11 @@ class PassportService implements S2sMessageHandlable {
   }
 
   async setupOidcStrategy() {
-
     this.resetOidcStrategy();
 
-    const isOidcEnabled = configManager.getConfig('security:passport-oidc:isEnabled');
+    const isOidcEnabled = configManager.getConfig(
+      'security:passport-oidc:isEnabled',
+    );
 
     // when disabled
     if (!isOidcEnabled) {
@@ -555,52 +641,79 @@ class PassportService implements S2sMessageHandlable {
 
     // setup client
     // extend oidc request timeouts
-    const OIDC_ISSUER_TIMEOUT_OPTION = await configManager.getConfig('security:passport-oidc:oidcIssuerTimeoutOption');
+    const OIDC_ISSUER_TIMEOUT_OPTION = await configManager.getConfig(
+      'security:passport-oidc:oidcIssuerTimeoutOption',
+    );
     // OIDCIssuer.defaultHttpOptions = { timeout: OIDC_ISSUER_TIMEOUT_OPTION };
 
     custom.setHttpOptionsDefaults({
       timeout: OIDC_ISSUER_TIMEOUT_OPTION,
     });
 
-    const issuerHost = configManager.getConfig('security:passport-oidc:issuerHost');
+    const issuerHost = configManager.getConfig(
+      'security:passport-oidc:issuerHost',
+    );
     const clientId = configManager.getConfig('security:passport-oidc:clientId');
-    const clientSecret = configManager.getConfig('security:passport-oidc:clientSecret');
-    const redirectUri = configManager.getConfig('app:siteUrl') != null
-      ? urljoin(growiInfoService.getSiteUrl(), '/passport/oidc/callback')
-      : configManager.getConfigLegacy<string>('security:passport-oidc:callbackUrl'); // DEPRECATED: backward compatible with v3.2.3 and below
+    const clientSecret = configManager.getConfig(
+      'security:passport-oidc:clientSecret',
+    );
+    const redirectUri =
+      configManager.getConfig('app:siteUrl') != null
+        ? urljoin(growiInfoService.getSiteUrl(), '/passport/oidc/callback')
+        : configManager.getConfigLegacy<string>(
+            'security:passport-oidc:callbackUrl',
+          ); // DEPRECATED: backward compatible with v3.2.3 and below
 
     // Prevent request timeout error on app init
     const oidcIssuer = await this.getOIDCIssuerInstance(issuerHost);
     if (clientId != null && oidcIssuer != null) {
       const oidcIssuerMetadata = oidcIssuer.metadata;
 
-      logger.debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
+      logger.debug(
+        'Discovered issuer %s %O',
+        oidcIssuer.issuer,
+        oidcIssuer.metadata,
+      );
 
-      const authorizationEndpoint = configManager.getConfig('security:passport-oidc:authorizationEndpoint');
+      const authorizationEndpoint = configManager.getConfig(
+        'security:passport-oidc:authorizationEndpoint',
+      );
       if (authorizationEndpoint) {
         oidcIssuerMetadata.authorization_endpoint = authorizationEndpoint;
       }
-      const tokenEndpoint = configManager.getConfig('security:passport-oidc:tokenEndpoint');
+      const tokenEndpoint = configManager.getConfig(
+        'security:passport-oidc:tokenEndpoint',
+      );
       if (tokenEndpoint) {
         oidcIssuerMetadata.token_endpoint = tokenEndpoint;
       }
-      const revocationEndpoint = configManager.getConfig('security:passport-oidc:revocationEndpoint');
+      const revocationEndpoint = configManager.getConfig(
+        'security:passport-oidc:revocationEndpoint',
+      );
       if (revocationEndpoint) {
         oidcIssuerMetadata.revocation_endpoint = revocationEndpoint;
       }
-      const introspectionEndpoint = configManager.getConfig('security:passport-oidc:introspectionEndpoint');
+      const introspectionEndpoint = configManager.getConfig(
+        'security:passport-oidc:introspectionEndpoint',
+      );
       if (introspectionEndpoint) {
         oidcIssuerMetadata.introspection_endpoint = introspectionEndpoint;
       }
-      const userInfoEndpoint = configManager.getConfig('security:passport-oidc:userInfoEndpoint');
+      const userInfoEndpoint = configManager.getConfig(
+        'security:passport-oidc:userInfoEndpoint',
+      );
       if (userInfoEndpoint) {
         oidcIssuerMetadata.userinfo_endpoint = userInfoEndpoint;
       }
-      const endSessionEndpoint = configManager.getConfig('security:passport-oidc:endSessionEndpoint');
+      const endSessionEndpoint = configManager.getConfig(
+        'security:passport-oidc:endSessionEndpoint',
+      );
       if (endSessionEndpoint) {
         oidcIssuerMetadata.end_session_endpoint = endSessionEndpoint;
       }
-      const registrationEndpoint = configManager.getConfig('security:passport-oidc:registrationEndpoint');
+      const registrationEndpoint = configManager.getConfig(
+        'security:passport-oidc:registrationEndpoint',
+      );
       if (registrationEndpoint) {
         oidcIssuerMetadata.registration_endpoint = registrationEndpoint;
       }
@@ -611,7 +724,11 @@ class PassportService implements S2sMessageHandlable {
 
       const newOidcIssuer = new OIDCIssuer(oidcIssuerMetadata);
 
-      logger.debug('Configured issuer %s %O', newOidcIssuer.issuer, newOidcIssuer.metadata);
+      logger.debug(
+        'Configured issuer %s %O',
+        newOidcIssuer.issuer,
+        newOidcIssuer.metadata,
+      );
 
       const client = new newOidcIssuer.Client({
         client_id: clientId,
@@ -621,26 +738,30 @@ class PassportService implements S2sMessageHandlable {
       });
       // prevent error AssertionError [ERR_ASSERTION]: id_token issued in the future
       // Doc: https://github.com/panva/node-openid-client/tree/v2.x#allow-for-system-clock-skew
-      const OIDC_CLIENT_CLOCK_TOLERANCE = await configManager.getConfig('security:passport-oidc:oidcClientClockTolerance');
+      const OIDC_CLIENT_CLOCK_TOLERANCE = await configManager.getConfig(
+        'security:passport-oidc:oidcClientClockTolerance',
+      );
       client[custom.clock_tolerance] = OIDC_CLIENT_CLOCK_TOLERANCE;
-      passport.use('oidc', new OidcStrategy(
-        {
-          client,
-          params: { scope: 'openid email profile' },
-        },
-        (tokenset, userinfo, done) => {
-          if (userinfo) {
-            return done(null, userinfo);
-          }
-
-          return done(null, false);
-        },
-      ));
+      passport.use(
+        'oidc',
+        new OidcStrategy(
+          {
+            client,
+            params: { scope: 'openid email profile' },
+          },
+          (tokenset, userinfo, done) => {
+            if (userinfo) {
+              return done(null, userinfo);
+            }
+
+            return done(null, false);
+          },
+        ),
+      );
 
       this.isOidcStrategySetup = true;
       logger.debug('OidcStrategy: setup is done');
     }
-
   }
 
   /**
@@ -663,7 +784,7 @@ class PassportService implements S2sMessageHandlable {
    * @param issuerHost string
    * @returns string URL/.well-known/openid-configuration
    */
-  getOIDCMetadataURL(issuerHost: string) : string {
+  getOIDCMetadataURL(issuerHost: string): string {
     const protocol = 'https://';
     const pattern = /^https?:\/\//i;
     const metadataPath = '/.well-known/openid-configuration';
@@ -672,20 +793,22 @@ class PassportService implements S2sMessageHandlable {
       return issuerHost;
     }
     // Set protocol if not available on url
-    const absUrl = !pattern.test(issuerHost) ? `${protocol}${issuerHost}` : issuerHost;
+    const absUrl = !pattern.test(issuerHost)
+      ? `${protocol}${issuerHost}`
+      : issuerHost;
     const url = new URL(absUrl).href;
     // Remove trailing slash if exists
     return `${url.replace(/\/+$/, '')}${metadataPath}`;
   }
 
   /**
- *
- * Check and initialize connection to OIDC issuer host
- * Prevent request timeout error on app init
- *
- * @param issuerHost string
- * @returns boolean
- */
+   *
+   * Check and initialize connection to OIDC issuer host
+   * Prevent request timeout error on app init
+   *
+   * @param issuerHost string
+   * @returns boolean
+   */
   async isOidcHostReachable(issuerHost: string): Promise<boolean | undefined> {
     try {
       const metadataUrl = this.getOIDCMetadataURL(issuerHost);
@@ -700,8 +823,7 @@ class PassportService implements S2sMessageHandlable {
         return false;
       }
       return true;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('OidcStrategy: issuer host unreachable:', err.code);
     }
   }
@@ -713,11 +835,20 @@ class PassportService implements S2sMessageHandlable {
    * @param issuerHost string
    * @returns instance of OIDCIssuer
    */
-  async getOIDCIssuerInstance(issuerHost: string | undefined): Promise<void | OIDCIssuer> {
-    const OIDC_TIMEOUT_MULTIPLIER = configManager.getConfig('security:passport-oidc:timeoutMultiplier');
-    const OIDC_DISCOVERY_RETRIES = configManager.getConfig('security:passport-oidc:discoveryRetries');
-    const OIDC_ISSUER_TIMEOUT_OPTION = configManager.getConfig('security:passport-oidc:oidcIssuerTimeoutOption');
-    const oidcIssuerHostReady = issuerHost != null && this.isOidcHostReachable(issuerHost);
+  async getOIDCIssuerInstance(
+    issuerHost: string | undefined,
+  ): Promise<void | OIDCIssuer> {
+    const OIDC_TIMEOUT_MULTIPLIER = configManager.getConfig(
+      'security:passport-oidc:timeoutMultiplier',
+    );
+    const OIDC_DISCOVERY_RETRIES = configManager.getConfig(
+      'security:passport-oidc:discoveryRetries',
+    );
+    const OIDC_ISSUER_TIMEOUT_OPTION = configManager.getConfig(
+      'security:passport-oidc:oidcIssuerTimeoutOption',
+    );
+    const oidcIssuerHostReady =
+      issuerHost != null && this.isOidcHostReachable(issuerHost);
 
     if (!oidcIssuerHostReady) {
       logger.error('OidcStrategy: setup failed');
@@ -725,33 +856,39 @@ class PassportService implements S2sMessageHandlable {
     }
 
     const metadataURL = this.getOIDCMetadataURL(issuerHost);
-    const oidcIssuer = await pRetry(async() => {
-      return OIDCIssuer.discover(metadataURL);
-    }, {
-      onFailedAttempt: (error) => {
-        // get current OIDCIssuer timeout options
-        OIDCIssuer[custom.http_options] = (url, options) => {
-          const timeout = options.timeout
-            ? options.timeout * OIDC_TIMEOUT_MULTIPLIER
-            : OIDC_ISSUER_TIMEOUT_OPTION * OIDC_TIMEOUT_MULTIPLIER;
-          custom.setHttpOptionsDefaults({ timeout });
-          return { timeout };
-        };
-
-        logger.debug(`OidcStrategy: setup attempt ${error.attemptNumber} failed with error: ${error}. Retrying ...`);
+    const oidcIssuer = await pRetry(
+      async () => {
+        return OIDCIssuer.discover(metadataURL);
       },
-      retries: OIDC_DISCOVERY_RETRIES,
-    }).catch((error) => {
+      {
+        onFailedAttempt: (error) => {
+          // get current OIDCIssuer timeout options
+          OIDCIssuer[custom.http_options] = (url, options) => {
+            const timeout = options.timeout
+              ? options.timeout * OIDC_TIMEOUT_MULTIPLIER
+              : OIDC_ISSUER_TIMEOUT_OPTION * OIDC_TIMEOUT_MULTIPLIER;
+            custom.setHttpOptionsDefaults({ timeout });
+            return { timeout };
+          };
+
+          logger.debug(
+            `OidcStrategy: setup attempt ${error.attemptNumber} failed with error: ${error}. Retrying ...`,
+          );
+        },
+        retries: OIDC_DISCOVERY_RETRIES,
+      },
+    ).catch((error) => {
       logger.error(`OidcStrategy: setup failed with error: ${error} `);
     });
     return oidcIssuer;
   }
 
   setupSamlStrategy(): void {
-
     this.resetSamlStrategy();
 
-    const isSamlEnabled = configManager.getConfig('security:passport-saml:isEnabled');
+    const isSamlEnabled = configManager.getConfig(
+      'security:passport-saml:isEnabled',
+    );
 
     // when disabled
     if (!isSamlEnabled) {
@@ -769,10 +906,16 @@ class PassportService implements S2sMessageHandlable {
     passport.use(
       new SamlStrategy(
         {
-          entryPoint: configManager.getConfig('security:passport-saml:entryPoint'),
-          callbackUrl: configManager.getConfig('app:siteUrl') != null
-            ? urljoin(growiInfoService.getSiteUrl(), '/passport/saml/callback') // auto-generated with v3.2.4 and above
-            : configManager.getConfig('security:passport-saml:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
+          entryPoint: configManager.getConfig(
+            'security:passport-saml:entryPoint',
+          ),
+          callbackUrl:
+            configManager.getConfig('app:siteUrl') != null
+              ? urljoin(
+                  growiInfoService.getSiteUrl(),
+                  '/passport/saml/callback',
+                ) // auto-generated with v3.2.4 and above
+              : configManager.getConfig('security:passport-saml:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
           issuer: configManager.getConfig('security:passport-saml:issuer'),
           cert,
           disableRequestedAuthnContext: true,
@@ -841,7 +984,9 @@ class PassportService implements S2sMessageHandlable {
     logger.debug({ 'Parsed Rule': JSON.stringify(luceneRule, null, 2) });
 
     const attributes = this.extractAttributesFromSAMLResponse(response);
-    logger.debug({ 'Extracted Attributes': JSON.stringify(attributes, null, 2) });
+    logger.debug({
+      'Extracted Attributes': JSON.stringify(attributes, null, 2),
+    });
 
     return this.evaluateRuleForSamlAttributes(attributes, luceneRule);
   }
@@ -858,7 +1003,12 @@ class PassportService implements S2sMessageHandlable {
 
     // when combined rules
     if (right != null) {
-      return this.evaluateCombinedRulesForSamlAttributes(attributes, left, right, operator);
+      return this.evaluateCombinedRulesForSamlAttributes(
+        attributes,
+        left,
+        right,
+        operator,
+      );
     }
     if (left != null) {
       return this.evaluateRuleForSamlAttributes(attributes, left);
@@ -891,15 +1041,29 @@ class PassportService implements S2sMessageHandlable {
    * @param {string} luceneOperator operator string expression
    * @see https://github.com/thoward/lucene-query-parser.js/wiki
    */
-  evaluateCombinedRulesForSamlAttributes(attributes, luceneRuleLeft, luceneRuleRight, luceneOperator) {
+  evaluateCombinedRulesForSamlAttributes(
+    attributes,
+    luceneRuleLeft,
+    luceneRuleRight,
+    luceneOperator,
+  ) {
     if (luceneOperator === 'OR') {
-      return this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) || this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight);
+      return (
+        this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) ||
+        this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight)
+      );
     }
     if (luceneOperator === 'AND') {
-      return this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) && this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight);
+      return (
+        this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) &&
+        this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight)
+      );
     }
     if (luceneOperator === 'NOT') {
-      return this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) && !this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight);
+      return (
+        this.evaluateRuleForSamlAttributes(attributes, luceneRuleLeft) &&
+        !this.evaluateRuleForSamlAttributes(attributes, luceneRuleRight)
+      );
     }
 
     throw new Error(`Unsupported operator: ${luceneOperator}`);
@@ -917,7 +1081,8 @@ class PassportService implements S2sMessageHandlable {
    * }
    */
   extractAttributesFromSAMLResponse(response) {
-    const attributeStatement = response.getAssertion().Assertion.AttributeStatement;
+    const attributeStatement =
+      response.getAssertion().Assertion.AttributeStatement;
     if (attributeStatement == null || attributeStatement[0] == null) {
       return {};
     }
@@ -930,11 +1095,10 @@ class PassportService implements S2sMessageHandlable {
     const result = {};
     for (const attribute of attributes) {
       const name = attribute.$.Name;
-      const attributeValues = attribute.AttributeValue.map(v => v._);
+      const attributeValues = attribute.AttributeValue.map((v) => v._);
       if (result[name] == null) {
         result[name] = attributeValues;
-      }
-      else {
+      } else {
         result[name] = result[name].concat(attributeValues);
       }
     }
@@ -961,7 +1125,7 @@ class PassportService implements S2sMessageHandlable {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       done(null, (user as any).id);
     });
-    passport.deserializeUser(async(id, done) => {
+    passport.deserializeUser(async (id, done) => {
       try {
         const user = await User.findById(id);
         if (user == null) {
@@ -972,8 +1136,7 @@ class PassportService implements S2sMessageHandlable {
           await user.save();
         }
         done(null, user);
-      }
-      catch (err) {
+      } catch (err) {
         done(err);
       }
     });
@@ -981,12 +1144,20 @@ class PassportService implements S2sMessageHandlable {
     this.isSerializerSetup = true;
   }
 
-  isSameUsernameTreatedAsIdenticalUser(providerType: IExternalAuthProviderType): boolean {
-    return configManager.getConfig(`security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`);
+  isSameUsernameTreatedAsIdenticalUser(
+    providerType: IExternalAuthProviderType,
+  ): boolean {
+    return configManager.getConfig(
+      `security:passport-${providerType}:isSameUsernameTreatedAsIdenticalUser`,
+    );
   }
 
-  isSameEmailTreatedAsIdenticalUser(providerType: Exclude<IExternalAuthProviderType, 'ldap'>): boolean {
-    return configManager.getConfig(`security:passport-${providerType}:isSameEmailTreatedAsIdenticalUser`);
+  isSameEmailTreatedAsIdenticalUser(
+    providerType: Exclude<IExternalAuthProviderType, 'ldap'>,
+  ): boolean {
+    return configManager.getConfig(
+      `security:passport-${providerType}:isSameEmailTreatedAsIdenticalUser`,
+    );
   }
 
   literalUnescape(string: string) {
@@ -1000,7 +1171,6 @@ class PassportService implements S2sMessageHandlable {
       .replace(/\\n/g, '\n')
       .replace(/\\r/g, '\r');
   }
-
 }
 
 export default PassportService;

+ 27 - 22
apps/app/src/server/service/pre-notify.ts

@@ -1,44 +1,52 @@
-import {
-  getIdForRef,
-  type IPage, type IUser, type Ref,
-} from '@growi/core';
+import { getIdForRef, type IPage, type IUser, type Ref } from '@growi/core';
 import mongoose from 'mongoose';
 
 import type { ActivityDocument } from '../models/activity';
 import Subscription from '../models/subscription';
 
 export type PreNotifyProps = {
-  notificationTargetUsers?: Ref<IUser>[],
-}
+  notificationTargetUsers?: Ref<IUser>[];
+};
 
 export type PreNotify = (props: PreNotifyProps) => Promise<void>;
-export type GetAdditionalTargetUsers = (activity: ActivityDocument) => Promise<Ref<IUser>[]>;
-export type GeneratePreNotify = (activity: ActivityDocument, getAdditionalTargetUsers?: GetAdditionalTargetUsers) => PreNotify;
+export type GetAdditionalTargetUsers = (
+  activity: ActivityDocument,
+) => Promise<Ref<IUser>[]>;
+export type GeneratePreNotify = (
+  activity: ActivityDocument,
+  getAdditionalTargetUsers?: GetAdditionalTargetUsers,
+) => PreNotify;
 
 interface IPreNotifyService {
-  generateInitialPreNotifyProps: (PreNotifyProps) => { notificationTargetUsers?: Ref<IUser>[] },
-  generatePreNotify: GeneratePreNotify
+  generateInitialPreNotifyProps: (PreNotifyProps) => {
+    notificationTargetUsers?: Ref<IUser>[];
+  };
+  generatePreNotify: GeneratePreNotify;
 }
 
 class PreNotifyService implements IPreNotifyService {
-
   generateInitialPreNotifyProps = (): PreNotifyProps => {
-
     const initialPreNotifyProps: Ref<IUser>[] = [];
 
     return { notificationTargetUsers: initialPreNotifyProps };
   };
 
-  generatePreNotify = (activity: ActivityDocument, getAdditionalTargetUsers?: GetAdditionalTargetUsers): PreNotify => {
-
-    const preNotify = async(props: PreNotifyProps) => {
+  generatePreNotify = (
+    activity: ActivityDocument,
+    getAdditionalTargetUsers?: GetAdditionalTargetUsers,
+  ): PreNotify => {
+    const preNotify = async (props: PreNotifyProps) => {
       const { notificationTargetUsers } = props;
 
-      const User = mongoose.model<IUser, { find, STATUS_ACTIVE }>('User');
+      const User = mongoose.model<IUser, { find; STATUS_ACTIVE }>('User');
       const actionUser = activity.user;
       const target = activity.target;
-      const subscribedUsers = await Subscription.getSubscription(target as unknown as Ref<IPage>);
-      const notificationUsers = subscribedUsers.filter(item => (item.toString() !== getIdForRef(actionUser).toString()));
+      const subscribedUsers = await Subscription.getSubscription(
+        target as unknown as Ref<IPage>,
+      );
+      const notificationUsers = subscribedUsers.filter(
+        (item) => item.toString() !== getIdForRef(actionUser).toString(),
+      );
       const activeNotificationUsers = await User.find({
         _id: { $in: notificationUsers },
         status: User.STATUS_ACTIVE,
@@ -46,8 +54,7 @@ class PreNotifyService implements IPreNotifyService {
 
       if (getAdditionalTargetUsers == null) {
         notificationTargetUsers?.push(...activeNotificationUsers);
-      }
-      else {
+      } else {
         const AdditionalTargetUsers = await getAdditionalTargetUsers(activity);
 
         notificationTargetUsers?.push(
@@ -55,12 +62,10 @@ class PreNotifyService implements IPreNotifyService {
           ...AdditionalTargetUsers,
         );
       }
-
     };
 
     return preNotify;
   };
-
 }
 
 export const preNotifyService = new PreNotifyService();

+ 8 - 10
apps/app/src/server/service/rest-qiita-API.js

@@ -16,7 +16,6 @@ function getAxios(team, token) {
  */
 
 class RestQiitaAPIService {
-
   /** @type {import('~/server/crowi').default} Crowi instance */
   crowi;
 
@@ -46,13 +45,12 @@ class RestQiitaAPIService {
    * @param {string} path
    */
   async restAPI(path) {
-    return this.axios.get(path)
-      .then((res) => {
-        const data = res.data;
-        const total = res.headers['total-count'];
+    return this.axios.get(path).then((res) => {
+      const data = res.data;
+      const total = res.headers['total-count'];
 
-        return { data, total };
-      });
+      return { data, total };
+    });
   }
 
   /**
@@ -68,7 +66,6 @@ class RestQiitaAPIService {
     }
   }
 
-
   /**
    * get Qiita pages
    * @memberof RestQiitaAPI
@@ -76,7 +73,9 @@ class RestQiitaAPIService {
    * @param {string} perPage
    */
   async getQiitaPages(pageNum, perPage) {
-    const res = await this.restAPI(`/items?page=${pageNum}&per_page=${perPage}`);
+    const res = await this.restAPI(
+      `/items?page=${pageNum}&per_page=${perPage}`,
+    );
     const pages = res.data;
     const total = res.total;
 
@@ -84,7 +83,6 @@ class RestQiitaAPIService {
       return { pages, total };
     }
   }
-
 }
 
 module.exports = RestQiitaAPIService;

+ 264 - 119
apps/app/src/server/service/search.ts

@@ -4,26 +4,36 @@ import mongoose from 'mongoose';
 import { FilterXSS } from 'xss';
 
 import { CommentEvent, commentEvent } from '~/features/comment/server';
-import { isIncludeAiMenthion, removeAiMenthion } from '~/features/search/utils/ai';
+import {
+  isIncludeAiMenthion,
+  removeAiMenthion,
+} from '~/features/search/utils/ai';
 import { SearchDelegatorName } from '~/interfaces/named-query';
-import type { IFormattedSearchResult, IPageWithSearchMeta, ISearchResult } from '~/interfaces/search';
+import type {
+  IFormattedSearchResult,
+  IPageWithSearchMeta,
+  ISearchResult,
+} from '~/interfaces/search';
 import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
 import type {
-  SearchDelegator, SearchQueryParser, SearchResolver, ParsedQuery, SearchableData, QueryTerms,
+  ParsedQuery,
+  QueryTerms,
+  SearchableData,
+  SearchDelegator,
+  SearchQueryParser,
+  SearchResolver,
 } from '../interfaces/search';
 import NamedQuery from '../models/named-query';
 import type { PageModel } from '../models/page';
 import { SearchError } from '../models/vo/search-error';
 import { hasIntersection } from '../util/compare-objectId';
-
 import { configManager } from './config-manager';
 import ElasticsearchDelegator from './search-delegator/elasticsearch';
 import PrivateLegacyPagesDelegator from './search-delegator/private-legacy-pages';
 
-
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:service:search');
 
@@ -41,8 +51,7 @@ const filterXss = new FilterXSS(filterXssOptions);
 
 const normalizeQueryString = (_queryString: string): string => {
   let queryString = _queryString.trim();
-  queryString = removeAiMenthion(queryString)
-    .replace(/\s+/g, ' ');
+  queryString = removeAiMenthion(queryString).replace(/\s+/g, ' ');
 
   return queryString;
 };
@@ -51,12 +60,14 @@ const normalizeNQName = (nqName: string): string => {
   return nqName.trim();
 };
 
-const findPageListByIds = async(pageIds: ObjectIdLike[], crowi: any) => {
-
+const findPageListByIds = async (pageIds: ObjectIdLike[], crowi: any) => {
   const Page = crowi.model('Page') as unknown as PageModel;
   const User = crowi.model('User');
 
-  const builder = new Page.PageQueryBuilder(Page.find(({ _id: { $in: pageIds } })), false);
+  const builder = new Page.PageQueryBuilder(
+    Page.find({ _id: { $in: pageIds } }),
+    false,
+  );
 
   builder.addConditionToPagenate(undefined, undefined); // offset and limit are unnesessary
 
@@ -73,11 +84,9 @@ const findPageListByIds = async(pageIds: ObjectIdLike[], crowi: any) => {
     pages,
     totalCount,
   };
-
 };
 
 class SearchService implements SearchQueryParser, SearchResolver {
-
   crowi: Crowi;
 
   isErrorOccuredOnHealthcheck: boolean | null;
@@ -86,7 +95,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
   fullTextSearchDelegator: any & ElasticsearchDelegator;
 
-  nqDelegators: {[key in SearchDelegatorName]: SearchDelegator};
+  nqDelegators: { [key in SearchDelegatorName]: SearchDelegator };
 
   constructor(crowi: Crowi) {
     this.crowi = crowi;
@@ -96,10 +105,11 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
     try {
       this.fullTextSearchDelegator = this.generateFullTextSearchDelegator();
-      this.nqDelegators = this.generateNQDelegators(this.fullTextSearchDelegator);
+      this.nqDelegators = this.generateNQDelegators(
+        this.fullTextSearchDelegator,
+      );
       logger.info('Succeeded to initialize search delegators');
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
     }
 
@@ -114,7 +124,11 @@ class SearchService implements SearchQueryParser, SearchResolver {
   }
 
   get isReachable() {
-    return this.isConfigured && !this.isErrorOccuredOnHealthcheck && !this.isErrorOccuredOnSearching;
+    return (
+      this.isConfigured &&
+      !this.isErrorOccuredOnHealthcheck &&
+      !this.isErrorOccuredOnSearching
+    );
   }
 
   get isElasticsearchEnabled() {
@@ -130,48 +144,130 @@ class SearchService implements SearchQueryParser, SearchResolver {
       return new ElasticsearchDelegator(this.crowi.socketIoService);
     }
 
-    logger.info('No elasticsearch URI is specified so that full text search is disabled.');
+    logger.info(
+      'No elasticsearch URI is specified so that full text search is disabled.',
+    );
   }
 
-  generateNQDelegators(defaultDelegator: ElasticsearchDelegator): {[key in SearchDelegatorName]: SearchDelegator} {
+  generateNQDelegators(defaultDelegator: ElasticsearchDelegator): {
+    [key in SearchDelegatorName]: SearchDelegator;
+  } {
     return {
       [SearchDelegatorName.DEFAULT]: defaultDelegator,
-      [SearchDelegatorName.PRIVATE_LEGACY_PAGES]: new PrivateLegacyPagesDelegator() as unknown as SearchDelegator,
+      [SearchDelegatorName.PRIVATE_LEGACY_PAGES]:
+        new PrivateLegacyPagesDelegator() as unknown as SearchDelegator,
     };
   }
 
   registerUpdateEvent() {
     const pageEvent = this.crowi.event('page');
-    pageEvent.on('create', this.fullTextSearchDelegator.syncPageUpdated.bind(this.fullTextSearchDelegator));
-    pageEvent.on('update', this.fullTextSearchDelegator.syncPageUpdated.bind(this.fullTextSearchDelegator));
+    pageEvent.on(
+      'create',
+      this.fullTextSearchDelegator.syncPageUpdated.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    pageEvent.on(
+      'update',
+      this.fullTextSearchDelegator.syncPageUpdated.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
     pageEvent.on('delete', (targetPage, deletedPage, user) => {
-      this.fullTextSearchDelegator.syncPageDeleted.bind(this.fullTextSearchDelegator)(targetPage, user);
-      this.fullTextSearchDelegator.syncPageUpdated.bind(this.fullTextSearchDelegator)(deletedPage, user);
+      this.fullTextSearchDelegator.syncPageDeleted.bind(
+        this.fullTextSearchDelegator,
+      )(targetPage, user);
+      this.fullTextSearchDelegator.syncPageUpdated.bind(
+        this.fullTextSearchDelegator,
+      )(deletedPage, user);
     });
     pageEvent.on('revert', (targetPage, revertedPage, user) => {
-      this.fullTextSearchDelegator.syncPageDeleted.bind(this.fullTextSearchDelegator)(targetPage, user);
-      this.fullTextSearchDelegator.syncPageUpdated.bind(this.fullTextSearchDelegator)(revertedPage, user);
+      this.fullTextSearchDelegator.syncPageDeleted.bind(
+        this.fullTextSearchDelegator,
+      )(targetPage, user);
+      this.fullTextSearchDelegator.syncPageUpdated.bind(
+        this.fullTextSearchDelegator,
+      )(revertedPage, user);
     });
-    pageEvent.on('deleteCompletely', this.fullTextSearchDelegator.syncPageDeleted.bind(this.fullTextSearchDelegator));
-    pageEvent.on('syncDescendantsDelete', this.fullTextSearchDelegator.syncDescendantsPagesDeleted.bind(this.fullTextSearchDelegator));
-    pageEvent.on('updateMany', this.fullTextSearchDelegator.syncPagesUpdated.bind(this.fullTextSearchDelegator));
-    pageEvent.on('syncDescendantsUpdate', this.fullTextSearchDelegator.syncDescendantsPagesUpdated.bind(this.fullTextSearchDelegator));
-    pageEvent.on('addSeenUsers', this.fullTextSearchDelegator.syncPageUpdated.bind(this.fullTextSearchDelegator));
+    pageEvent.on(
+      'deleteCompletely',
+      this.fullTextSearchDelegator.syncPageDeleted.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    pageEvent.on(
+      'syncDescendantsDelete',
+      this.fullTextSearchDelegator.syncDescendantsPagesDeleted.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    pageEvent.on(
+      'updateMany',
+      this.fullTextSearchDelegator.syncPagesUpdated.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    pageEvent.on(
+      'syncDescendantsUpdate',
+      this.fullTextSearchDelegator.syncDescendantsPagesUpdated.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    pageEvent.on(
+      'addSeenUsers',
+      this.fullTextSearchDelegator.syncPageUpdated.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
     pageEvent.on('rename', () => {
-      this.fullTextSearchDelegator.syncPageDeleted.bind(this.fullTextSearchDelegator);
-      this.fullTextSearchDelegator.syncPageUpdated.bind(this.fullTextSearchDelegator);
+      this.fullTextSearchDelegator.syncPageDeleted.bind(
+        this.fullTextSearchDelegator,
+      );
+      this.fullTextSearchDelegator.syncPageUpdated.bind(
+        this.fullTextSearchDelegator,
+      );
     });
 
     const bookmarkEvent = this.crowi.event('bookmark');
-    bookmarkEvent.on('create', this.fullTextSearchDelegator.syncBookmarkChanged.bind(this.fullTextSearchDelegator));
-    bookmarkEvent.on('delete', this.fullTextSearchDelegator.syncBookmarkChanged.bind(this.fullTextSearchDelegator));
+    bookmarkEvent.on(
+      'create',
+      this.fullTextSearchDelegator.syncBookmarkChanged.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    bookmarkEvent.on(
+      'delete',
+      this.fullTextSearchDelegator.syncBookmarkChanged.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
 
     const tagEvent = this.crowi.event('tag');
-    tagEvent.on('update', this.fullTextSearchDelegator.syncTagChanged.bind(this.fullTextSearchDelegator));
-
-    commentEvent.on(CommentEvent.CREATE, this.fullTextSearchDelegator.syncCommentChanged.bind(this.fullTextSearchDelegator));
-    commentEvent.on(CommentEvent.UPDATE, this.fullTextSearchDelegator.syncCommentChanged.bind(this.fullTextSearchDelegator));
-    commentEvent.on(CommentEvent.DELETE, this.fullTextSearchDelegator.syncCommentChanged.bind(this.fullTextSearchDelegator));
+    tagEvent.on(
+      'update',
+      this.fullTextSearchDelegator.syncTagChanged.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+
+    commentEvent.on(
+      CommentEvent.CREATE,
+      this.fullTextSearchDelegator.syncCommentChanged.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    commentEvent.on(
+      CommentEvent.UPDATE,
+      this.fullTextSearchDelegator.syncCommentChanged.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
+    commentEvent.on(
+      CommentEvent.DELETE,
+      this.fullTextSearchDelegator.syncCommentChanged.bind(
+        this.fullTextSearchDelegator,
+      ),
+    );
   }
 
   resetErrorStatus() {
@@ -188,8 +284,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
       logger.info('Reconnecting succeeded.');
       this.resetErrorStatus();
-    }
-    catch (err) {
+    } catch (err) {
       throw err;
     }
   }
@@ -197,8 +292,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
   async getInfo() {
     try {
       return await this.fullTextSearchDelegator.getInfo();
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw err;
     }
@@ -210,8 +304,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
       this.isErrorOccuredOnHealthcheck = false;
       return result;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
 
       // switch error flag, `isErrorOccuredOnHealthcheck` to be `false`
@@ -232,7 +325,10 @@ class SearchService implements SearchQueryParser, SearchResolver {
     return this.fullTextSearchDelegator.rebuildIndex();
   }
 
-  async parseSearchQuery(queryString: string, nqName: string | null): Promise<ParsedQuery> {
+  async parseSearchQuery(
+    queryString: string,
+    nqName: string | null,
+  ): Promise<ParsedQuery> {
     // eslint-disable-next-line no-param-reassign
     queryString = normalizeQueryString(queryString);
 
@@ -246,7 +342,9 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
     // will delegate to full-text search
     if (nq == null) {
-      logger.debug(`Delegated to full-text search since a named query document did not found. (nqName="${nqName}")`);
+      logger.debug(
+        `Delegated to full-text search since a named query document did not found. (nqName="${nqName}")`,
+      );
       return { queryString, terms };
     }
 
@@ -254,17 +352,25 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
     let parsedQuery: ParsedQuery;
     if (aliasOf != null) {
-      parsedQuery = { queryString: normalizeQueryString(aliasOf), terms: this.parseQueryString(aliasOf) };
-    }
-    else {
+      parsedQuery = {
+        queryString: normalizeQueryString(aliasOf),
+        terms: this.parseQueryString(aliasOf),
+      };
+    } else {
       parsedQuery = { queryString, terms, delegatorName };
     }
 
     return parsedQuery;
   }
 
-  async resolve(parsedQuery: ParsedQuery): Promise<[SearchDelegator, SearchableData]> {
-    const { queryString, terms, delegatorName = SearchDelegatorName.DEFAULT } = parsedQuery;
+  async resolve(
+    parsedQuery: ParsedQuery,
+  ): Promise<[SearchDelegator, SearchableData]> {
+    const {
+      queryString,
+      terms,
+      delegatorName = SearchDelegatorName.DEFAULT,
+    } = parsedQuery;
     const nqDeledator = this.nqDelegators[delegatorName];
 
     const data = {
@@ -280,7 +386,10 @@ class SearchService implements SearchQueryParser, SearchResolver {
    * @param {SearchDelegator} delegator
    * @throws {SearchError} SearchError
    */
-  private validateSearchableData(delegator: SearchDelegator, data: SearchableData): void {
+  private validateSearchableData(
+    delegator: SearchDelegator,
+    data: SearchableData,
+  ): void {
     const { terms } = data;
 
     if (delegator.isTermsNormalized(terms)) {
@@ -289,16 +398,24 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
     const unavailableTermsKeys = delegator.validateTerms(terms);
 
-    throw new SearchError('The query string includes unavailable terms.', unavailableTermsKeys);
+    throw new SearchError(
+      'The query string includes unavailable terms.',
+      unavailableTermsKeys,
+    );
   }
 
-  async searchKeyword(keyword: string, nqName: string | null, user, userGroups, searchOpts): Promise<[ISearchResult<unknown>, string | null]> {
+  async searchKeyword(
+    keyword: string,
+    nqName: string | null,
+    user,
+    userGroups,
+    searchOpts,
+  ): Promise<[ISearchResult<unknown>, string | null]> {
     let parsedQuery: ParsedQuery;
     // parse
     try {
       parsedQuery = await this.parseSearchQuery(keyword, nqName);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Error occurred while parseSearchQuery', err);
       throw err;
     }
@@ -312,8 +429,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
     // resolve
     try {
       [delegator, data] = await this.resolve(parsedQuery);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Error occurred while resolving search delegator', err);
       throw err;
     }
@@ -321,7 +437,10 @@ class SearchService implements SearchQueryParser, SearchResolver {
     // throws
     this.validateSearchableData(delegator, data);
 
-    return [await delegator.search(data, user, userGroups, searchOpts), delegator.name ?? null];
+    return [
+      await delegator.search(data, user, userGroups, searchOpts),
+      delegator.name ?? null,
+    ];
   }
 
   parseQueryString(queryString: string): QueryTerms {
@@ -346,8 +465,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
         phrase.trim();
         if (phrase.match(/^-/)) {
           notPhraseWords.push(phrase.replace(/^-/, ''));
-        }
-        else {
+        } else {
           phraseWords.push(phrase);
         }
       });
@@ -367,22 +485,17 @@ class SearchService implements SearchQueryParser, SearchResolver {
       if (matchNegative != null) {
         if (matchNegative[1] === 'prefix:') {
           notPrefixPaths.push(matchNegative[2]);
-        }
-        else if (matchNegative[1] === 'tag:') {
+        } else if (matchNegative[1] === 'tag:') {
           notTags.push(matchNegative[2]);
-        }
-        else {
+        } else {
           notMatchWords.push(matchNegative[2]);
         }
-      }
-      else if (matchPositive != null) {
+      } else if (matchPositive != null) {
         if (matchPositive[1] === 'prefix:') {
           prefixPaths.push(matchPositive[2]);
-        }
-        else if (matchPositive[1] === 'tag:') {
+        } else if (matchPositive[1] === 'tag:') {
           tags.push(matchPositive[2]);
-        }
-        else {
+        } else {
           matchWords.push(matchPositive[2]);
         }
       }
@@ -404,14 +517,22 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
   // TODO: optimize the way to check isFormattable e.g. check data schema of searchResult
   // So far, it determines by delegatorName passed by searchService.searchKeyword
-  checkIsFormattable(searchResult, delegatorName: SearchDelegatorName): boolean {
+  checkIsFormattable(
+    searchResult,
+    delegatorName: SearchDelegatorName,
+  ): boolean {
     return delegatorName === SearchDelegatorName.DEFAULT;
   }
 
   /**
    * formatting result
    */
-  async formatSearchResult(searchResult: ISearchResult<any>, delegatorName: SearchDelegatorName, user, userGroups): Promise<IFormattedSearchResult> {
+  async formatSearchResult(
+    searchResult: ISearchResult<any>,
+    delegatorName: SearchDelegatorName,
+    user,
+    userGroups,
+  ): Promise<IFormattedSearchResult> {
     if (!this.checkIsFormattable(searchResult, delegatorName)) {
       const data: IPageWithSearchMeta[] = searchResult.data.map((page) => {
         return {
@@ -432,7 +553,9 @@ class SearchService implements SearchQueryParser, SearchResolver {
     const result = {} as IFormattedSearchResult;
 
     // get page data
-    const pageIds: string[] = searchResult.data.map((page) => { return page._id });
+    const pageIds: string[] = searchResult.data.map((page) => {
+      return page._id;
+    });
 
     const findPageResult = await findPageListByIds(pageIds, this.crowi);
 
@@ -440,53 +563,73 @@ class SearchService implements SearchQueryParser, SearchResolver {
     result.meta = searchResult.meta;
 
     // set search result page data
-    const pages: (IPageWithSearchMeta | null)[] = searchResult.data.map((data) => {
-      const pageData = findPageResult.pages.find((pageData) => {
-        return pageData.id === data._id;
-      });
+    const pages: (IPageWithSearchMeta | null)[] = searchResult.data.map(
+      (data) => {
+        const pageData = findPageResult.pages.find((pageData) => {
+          return pageData.id === data._id;
+        });
+
+        if (pageData == null) {
+          return null;
+        }
 
-      if (pageData == null) {
-        return null;
-      }
+        // add tags and seenUserCount to pageData
+        pageData._doc.tags = data._source.tag_names;
+        pageData._doc.seenUserCount =
+          (pageData.seenUsers && pageData.seenUsers.length) || 0;
+
+        // serialize lastUpdateUser
+        if (
+          pageData.lastUpdateUser != null &&
+          pageData.lastUpdateUser instanceof User
+        ) {
+          pageData.lastUpdateUser = serializeUserSecurely(
+            pageData.lastUpdateUser,
+          );
+        }
 
-      // add tags and seenUserCount to pageData
-      pageData._doc.tags = data._source.tag_names;
-      pageData._doc.seenUserCount = (pageData.seenUsers && pageData.seenUsers.length) || 0;
+        // increment elasticSearchResult
+        let elasticSearchResult;
+        const highlightData = data._highlight;
+        if (highlightData != null) {
+          const snippet = this.canShowSnippet(pageData, user, userGroups)
+            ? // eslint-disable-next-line max-len
+              highlightData.body ||
+              highlightData['body.en'] ||
+              highlightData['body.ja'] ||
+              highlightData.comments ||
+              highlightData['comments.en'] ||
+              highlightData['comments.ja']
+            : null;
+          const pathMatch =
+            highlightData['path.en'] || highlightData['path.ja'];
+
+          elasticSearchResult = {
+            snippet:
+              snippet != null && typeof snippet[0] === 'string'
+                ? filterXss.process(snippet)
+                : null,
+            highlightedPath:
+              pathMatch != null && typeof pathMatch[0] === 'string'
+                ? filterXss.process(pathMatch)
+                : null,
+          };
+        }
 
-      // serialize lastUpdateUser
-      if (pageData.lastUpdateUser != null && pageData.lastUpdateUser instanceof User) {
-        pageData.lastUpdateUser = serializeUserSecurely(pageData.lastUpdateUser);
-      }
+        // serialize creator
+        if (pageData.creator != null && pageData.creator instanceof User) {
+          pageData.creator = serializeUserSecurely(pageData.creator);
+        }
 
-      // increment elasticSearchResult
-      let elasticSearchResult;
-      const highlightData = data._highlight;
-      if (highlightData != null) {
-        const snippet = this.canShowSnippet(pageData, user, userGroups)
-          // eslint-disable-next-line max-len
-          ? highlightData.body || highlightData['body.en'] || highlightData['body.ja'] || highlightData.comments || highlightData['comments.en'] || highlightData['comments.ja']
-          : null;
-        const pathMatch = highlightData['path.en'] || highlightData['path.ja'];
-
-        elasticSearchResult = {
-          snippet: snippet != null && typeof snippet[0] === 'string' ? filterXss.process(snippet) : null,
-          highlightedPath: pathMatch != null && typeof pathMatch[0] === 'string' ? filterXss.process(pathMatch) : null,
+        // generate pageMeta data
+        const pageMeta = {
+          bookmarkCount: data._source.bookmark_count || 0,
+          elasticSearchResult,
         };
-      }
 
-      // serialize creator
-      if (pageData.creator != null && pageData.creator instanceof User) {
-        pageData.creator = serializeUserSecurely(pageData.creator);
-      }
-
-      // generate pageMeta data
-      const pageMeta = {
-        bookmarkCount: data._source.bookmark_count || 0,
-        elasticSearchResult,
-      };
-
-      return { data: pageData, meta: pageMeta };
-    });
+        return { data: pageData, meta: pageMeta };
+      },
+    );
 
     result.data = pages.filter(nonNullable);
     return result;
@@ -512,12 +655,14 @@ class SearchService implements SearchQueryParser, SearchResolver {
     if (testGrant === Page.GRANT_USER_GROUP) {
       if (userGroups == null) return false;
 
-      return hasIntersection(userGroups.map(id => id.toString()), testGrantedGroups);
+      return hasIntersection(
+        userGroups.map((id) => id.toString()),
+        testGrantedGroups,
+      );
     }
 
     return true;
   }
-
 }
 
 export default SearchService;

+ 112 - 50
apps/app/src/server/service/slack-integration.ts

@@ -1,16 +1,16 @@
 import {
-  SlackbotType, type GrowiCommand, type GrowiBotEvent,
+  type GrowiBotEvent,
+  type GrowiCommand,
+  SlackbotType,
 } from '@growi/slack';
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 import type { InteractionPayloadAccessor } from '@growi/slack/dist/utils/interaction-payload-accessor';
 import type { RespondUtil } from '@growi/slack/dist/utils/respond-util-factory';
 import { generateWebClient } from '@growi/slack/dist/utils/webclient-factory';
-import type { WebClient } from '@slack/web-api';
-import { type ChatPostMessageArguments } from '@slack/web-api';
+import type { ChatPostMessageArguments, WebClient } from '@slack/web-api';
 import type { IncomingWebhookSendArguments } from '@slack/webhook';
 import mongoose from 'mongoose';
 
-
 import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
@@ -18,7 +18,6 @@ import type { EventActionsPermission } from '../interfaces/slack-integration/eve
 import S2sMessage from '../models/vo/s2s-message';
 import { SlackCommandHandlerError } from '../models/vo/slack-command-handler-error';
 import { slackLegacyUtilFactory } from '../util/slack-legacy';
-
 import { configManager } from './config-manager';
 import type { S2sMessagingService } from './s2s-messaging/base';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
@@ -31,7 +30,6 @@ const OFFICIAL_SLACKBOT_PROXY_URI = 'https://slackbot-proxy.growi.org';
 type S2sMessageForSlackIntegration = S2sMessage & { updatedAt: Date };
 
 export class SlackIntegrationService implements S2sMessageHandlable {
-
   crowi: Crowi;
 
   s2sMessagingService!: S2sMessagingService;
@@ -61,10 +59,12 @@ export class SlackIntegrationService implements S2sMessageHandlable {
       return false;
     }
 
-    return this.lastLoadedAt == null || this.lastLoadedAt < new Date(s2sMessage.updatedAt);
+    return (
+      this.lastLoadedAt == null ||
+      this.lastLoadedAt < new Date(s2sMessage.updatedAt)
+    );
   }
 
-
   /**
    * @inheritdoc
    */
@@ -80,13 +80,17 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     const { s2sMessagingService } = this;
 
     if (s2sMessagingService != null) {
-      const s2sMessage = new S2sMessage('slackIntegrationServiceUpdated', { updatedAt: new Date() });
+      const s2sMessage = new S2sMessage('slackIntegrationServiceUpdated', {
+        updatedAt: new Date(),
+      });
 
       try {
         await s2sMessagingService.publish(s2sMessage);
-      }
-      catch (e) {
-        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      } catch (e) {
+        logger.error(
+          'Failed to publish update message with S2sMessagingService: ',
+          e.message,
+        );
       }
     }
   }
@@ -96,14 +100,18 @@ export class SlackIntegrationService implements S2sMessageHandlable {
   }
 
   get isSlackbotConfigured(): boolean {
-    const hasSlackbotType = !!configManager.getConfig('slackbot:currentBotType');
+    const hasSlackbotType = !!configManager.getConfig(
+      'slackbot:currentBotType',
+    );
     return hasSlackbotType;
   }
 
   get isSlackLegacyConfigured(): boolean {
     // for legacy util
     const hasSlackToken = !!configManager.getConfig('slack:token');
-    const hasSlackIwhUrl = !!configManager.getConfig('slack:incomingWebhookUrl');
+    const hasSlackIwhUrl = !!configManager.getConfig(
+      'slack:incomingWebhookUrl',
+    );
 
     return hasSlackToken || hasSlackIwhUrl;
   }
@@ -111,7 +119,9 @@ export class SlackIntegrationService implements S2sMessageHandlable {
   private isCheckTypeValid(): boolean {
     const currentBotType = configManager.getConfig('slackbot:currentBotType');
     if (currentBotType == null) {
-      throw new Error('The config \'SLACKBOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
+      throw new Error(
+        "The config 'SLACKBOT_TYPE'(ns: 'crowi', key: 'slackbot:currentBotType') must be set.",
+      );
     }
 
     return true;
@@ -145,7 +155,9 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     const token = configManager.getConfig('slackbot:withoutProxy:botToken');
 
     if (token == null) {
-      throw new Error('The config \'SLACK_BOT_TOKEN\'(ns: \'crowi\', key: \'slackbot:withoutProxy:botToken\') must be set.');
+      throw new Error(
+        "The config 'SLACK_BOT_TOKEN'(ns: 'crowi', key: 'slackbot:withoutProxy:botToken') must be set.",
+      );
     }
 
     return generateWebClient(token);
@@ -160,13 +172,19 @@ export class SlackIntegrationService implements S2sMessageHandlable {
 
     const SlackAppIntegration = mongoose.model('SlackAppIntegration');
 
-    const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+    const slackAppIntegration = await SlackAppIntegration.findOne({
+      tokenPtoG,
+    });
 
     if (slackAppIntegration == null) {
-      throw new Error('No SlackAppIntegration exists that corresponds to the tokenPtoG specified.');
+      throw new Error(
+        'No SlackAppIntegration exists that corresponds to the tokenPtoG specified.',
+      );
     }
 
-    return this.generateClientBySlackAppIntegration(slackAppIntegration as unknown as { tokenGtoP: string; });
+    return this.generateClientBySlackAppIntegration(
+      slackAppIntegration as unknown as { tokenGtoP: string },
+    );
   }
 
   /**
@@ -184,20 +202,26 @@ export class SlackIntegrationService implements S2sMessageHandlable {
 
     // retrieve primary SlackAppIntegration
     const SlackAppIntegration = mongoose.model('SlackAppIntegration');
-    const slackAppIntegration = await SlackAppIntegration.findOne({ isPrimary: true });
+    const slackAppIntegration = await SlackAppIntegration.findOne({
+      isPrimary: true,
+    });
 
     if (slackAppIntegration == null) {
       throw new Error('None of the primary SlackAppIntegration exists.');
     }
 
-    return this.generateClientBySlackAppIntegration(slackAppIntegration as unknown as { tokenGtoP: string; });
+    return this.generateClientBySlackAppIntegration(
+      slackAppIntegration as unknown as { tokenGtoP: string },
+    );
   }
 
   /**
    * generate WebClient instance by SlackAppIntegration
    * @param slackAppIntegration
    */
-  async generateClientBySlackAppIntegration(slackAppIntegration: { tokenGtoP: string }): Promise<WebClient> {
+  async generateClientBySlackAppIntegration(slackAppIntegration: {
+    tokenGtoP: string;
+  }): Promise<WebClient> {
     this.isCheckTypeValid();
 
     // connect to proxy
@@ -209,33 +233,37 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     return generateWebClient(undefined, serverUri.toString(), headers);
   }
 
-  async postMessage(messageArgs: ChatPostMessageArguments, slackAppIntegration?: { tokenGtoP: string; }): Promise<void> {
+  async postMessage(
+    messageArgs: ChatPostMessageArguments,
+    slackAppIntegration?: { tokenGtoP: string },
+  ): Promise<void> {
     // use legacy slack configuration
     if (this.isSlackLegacyConfigured && !this.isSlackbotConfigured) {
       return this.postMessageWithLegacyUtil(messageArgs);
     }
 
-    const client = slackAppIntegration == null
-      ? await this.generateClientForPrimaryWorkspace()
-      : await this.generateClientBySlackAppIntegration(slackAppIntegration);
+    const client =
+      slackAppIntegration == null
+        ? await this.generateClientForPrimaryWorkspace()
+        : await this.generateClientBySlackAppIntegration(slackAppIntegration);
 
     try {
       await client.chat.postMessage(messageArgs);
-    }
-    catch (error) {
+    } catch (error) {
       logger.debug('Post error', error);
       logger.debug('Sent data to slack is:', messageArgs);
       throw error;
     }
   }
 
-  private async postMessageWithLegacyUtil(messageArgs: ChatPostMessageArguments | IncomingWebhookSendArguments): Promise<void> {
+  private async postMessageWithLegacyUtil(
+    messageArgs: ChatPostMessageArguments | IncomingWebhookSendArguments,
+  ): Promise<void> {
     const slackLegacyUtil = slackLegacyUtilFactory(configManager);
 
     try {
       await slackLegacyUtil.postMessage(messageArgs);
-    }
-    catch (error) {
+    } catch (error) {
       logger.debug('Post error', error);
       logger.debug('Sent data to slack is:', messageArgs);
       throw error;
@@ -245,22 +273,28 @@ export class SlackIntegrationService implements S2sMessageHandlable {
   /**
    * Handle /commands endpoint
    */
-  async handleCommandRequest(growiCommand: GrowiCommand, client, body, respondUtil: RespondUtil): Promise<void> {
+  async handleCommandRequest(
+    growiCommand: GrowiCommand,
+    client,
+    body,
+    respondUtil: RespondUtil,
+  ): Promise<void> {
     const { growiCommandType } = growiCommand;
     const modulePath = `./slack-command-handler/${growiCommandType}`;
 
     let handler;
     try {
       handler = require(modulePath)(this.crowi);
-    }
-    catch (err) {
+    } catch (err) {
       const text = `*No command.*\n \`command: ${growiCommand.text}\``;
       logger.error(err);
       throw new SlackCommandHandlerError(text, {
         respondBody: {
           text,
           blocks: [
-            markdownSectionBlock('*No command.*\n Hint\n `/growi [command] [keyword]`'),
+            markdownSectionBlock(
+              '*No command.*\n Hint\n `/growi [command] [keyword]`',
+            ),
           ],
         },
       });
@@ -271,9 +305,13 @@ export class SlackIntegrationService implements S2sMessageHandlable {
   }
 
   async handleBlockActionsRequest(
-      client, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor, respondUtil: RespondUtil,
+    client,
+    interactionPayload: any,
+    interactionPayloadAccessor: InteractionPayloadAccessor,
+    respondUtil: RespondUtil,
   ): Promise<void> {
-    const { actionId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
+    const { actionId } =
+      interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
     const commandName = actionId.split(':')[0];
     const handlerMethodName = actionId.split(':')[1];
 
@@ -282,19 +320,30 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     let handler;
     try {
       handler = require(modulePath)(this.crowi);
-    }
-    catch (err) {
-      throw new SlackCommandHandlerError(`No interaction.\n \`actionId: ${actionId}\``);
+    } catch (err) {
+      throw new SlackCommandHandlerError(
+        `No interaction.\n \`actionId: ${actionId}\``,
+      );
     }
 
     // Do not wrap with try-catch. Errors thrown by slack-command-handler modules will be handled in router.
-    return handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName, respondUtil);
+    return handler.handleInteractions(
+      client,
+      interactionPayload,
+      interactionPayloadAccessor,
+      handlerMethodName,
+      respondUtil,
+    );
   }
 
   async handleViewSubmissionRequest(
-      client, interactionPayload: any, interactionPayloadAccessor: InteractionPayloadAccessor, respondUtil: RespondUtil,
+    client,
+    interactionPayload: any,
+    interactionPayloadAccessor: InteractionPayloadAccessor,
+    respondUtil: RespondUtil,
   ): Promise<void> {
-    const { callbackId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
+    const { callbackId } =
+      interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
     const commandName = callbackId.split(':')[0];
     const handlerMethodName = callbackId.split(':')[1];
 
@@ -303,16 +352,28 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     let handler;
     try {
       handler = require(modulePath)(this.crowi);
-    }
-    catch (err) {
-      throw new SlackCommandHandlerError(`No interaction.\n \`callbackId: ${callbackId}\``);
+    } catch (err) {
+      throw new SlackCommandHandlerError(
+        `No interaction.\n \`callbackId: ${callbackId}\``,
+      );
     }
 
     // Do not wrap with try-catch. Errors thrown by slack-command-handler modules will be handled in router.
-    return handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName, respondUtil);
+    return handler.handleInteractions(
+      client,
+      interactionPayload,
+      interactionPayloadAccessor,
+      handlerMethodName,
+      respondUtil,
+    );
   }
 
-  async handleEventsRequest(client: WebClient, growiBotEvent: GrowiBotEvent<any>, permission: EventActionsPermission, data?: any): Promise<void> {
+  async handleEventsRequest(
+    client: WebClient,
+    growiBotEvent: GrowiBotEvent<any>,
+    permission: EventActionsPermission,
+    data?: any,
+  ): Promise<void> {
     const { eventType } = growiBotEvent;
     const { channel = '' } = growiBotEvent.event; // only channelId
 
@@ -320,7 +381,8 @@ export class SlackIntegrationService implements S2sMessageHandlable {
       return this.linkSharedHandler.handleEvent(client, growiBotEvent, data);
     }
 
-    logger.error(`Any event actions are not permitted, or, a handler for '${eventType}' event is not implemented`);
+    logger.error(
+      `Any event actions are not permitted, or, a handler for '${eventType}' event is not implemented`,
+    );
   }
-
 }

+ 101 - 35
apps/app/src/server/service/user-group.ts

@@ -1,33 +1,54 @@
-import type { IUser, IGrantedGroup } from '@growi/core';
+import type { IGrantedGroup, IUser } from '@growi/core';
 import type { DeleteResult } from 'mongodb';
 import mongoose, { type Model } from 'mongoose';
 
 import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import type { UserGroupDocument, UserGroupModel } from '~/server/models/user-group';
+import type {
+  UserGroupDocument,
+  UserGroupModel,
+} from '~/server/models/user-group';
 import UserGroup from '~/server/models/user-group';
-import { excludeTestIdsFromTargetIds, includesObjectIds } from '~/server/util/compare-objectId';
+import {
+  excludeTestIdsFromTargetIds,
+  includesObjectIds,
+} from '~/server/util/compare-objectId';
 import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
-import type { UserGroupRelationDocument, UserGroupRelationModel } from '../models/user-group-relation';
+import type {
+  UserGroupRelationDocument,
+  UserGroupRelationModel,
+} from '../models/user-group-relation';
 import UserGroupRelation from '../models/user-group-relation';
 
-
 const logger = loggerFactory('growi:service:UserGroupService'); // eslint-disable-line no-unused-vars
 
 export interface IUserGroupService {
   init(): Promise<void>;
-  updateGroup(id: ObjectIdLike, name?: string, description?: string, parentId?: ObjectIdLike | null, forceUpdateParents?: boolean): Promise<UserGroupDocument>;
-  removeCompletelyByRootGroupId(deleteRootGroupId: ObjectIdLike, action: string, user: IUser, transferToUserGroup?: IGrantedGroup): Promise<DeleteResult>;
-  removeUserByUsername(userGroupId: ObjectIdLike, username: string): Promise<{user: IUser, deletedGroupsCount: number}>;
+  updateGroup(
+    id: ObjectIdLike,
+    name?: string,
+    description?: string,
+    parentId?: ObjectIdLike | null,
+    forceUpdateParents?: boolean,
+  ): Promise<UserGroupDocument>;
+  removeCompletelyByRootGroupId(
+    deleteRootGroupId: ObjectIdLike,
+    action: string,
+    user: IUser,
+    transferToUserGroup?: IGrantedGroup,
+  ): Promise<DeleteResult>;
+  removeUserByUsername(
+    userGroupId: ObjectIdLike,
+    username: string,
+  ): Promise<{ user: IUser; deletedGroupsCount: number }>;
 }
 
 /**
  * the service class of UserGroupService
  */
 class UserGroupService implements IUserGroupService {
-
   crowi: Crowi;
 
   constructor(crowi: Crowi) {
@@ -40,7 +61,13 @@ class UserGroupService implements IUserGroupService {
   }
 
   // ref: https://dev.growi.org/61b2cdabaa330ce7d8152844
-  async updateGroup(id, name?: string, description?: string, parentId?: string | null, forceUpdateParents = false): Promise<UserGroupDocument> {
+  async updateGroup(
+    id,
+    name?: string,
+    description?: string,
+    parentId?: string | null,
+    forceUpdateParents = false,
+  ): Promise<UserGroupDocument> {
     const userGroup = await UserGroup.findById(id);
     if (userGroup == null) {
       throw new Error('The group does not exist');
@@ -67,7 +94,8 @@ class UserGroupService implements IUserGroupService {
     /*
      * Update parent
      */
-    if (parentId === undefined) { // undefined will be ignored
+    if (parentId === undefined) {
+      // undefined will be ignored
       return userGroup.save();
     }
 
@@ -77,10 +105,10 @@ class UserGroupService implements IUserGroupService {
       return userGroup.save();
     }
 
-
     const parent = await UserGroup.findById(parentId);
 
-    if (parent == null) { // it should not be null
+    if (parent == null) {
+      // it should not be null
       throw Error('Parent group does not exist.');
     }
 
@@ -89,16 +117,26 @@ class UserGroupService implements IUserGroupService {
      */
 
     // throw if parent was in self and its descendants
-    const descendantsWithTarget = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
-    if (includesObjectIds(descendantsWithTarget.map(d => d._id), [parent._id])) {
+    const descendantsWithTarget =
+      await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
+    if (
+      includesObjectIds(
+        descendantsWithTarget.map((d) => d._id),
+        [parent._id],
+      )
+    ) {
       throw Error('It is not allowed to choose parent from descendant groups.');
     }
 
     // find users for comparison
-    const [targetGroupUsers, parentGroupUsers] = await Promise.all(
-      [UserGroupRelation.findUserIdsByGroupId(userGroup._id), UserGroupRelation.findUserIdsByGroupId(parent._id)],
+    const [targetGroupUsers, parentGroupUsers] = await Promise.all([
+      UserGroupRelation.findUserIdsByGroupId(userGroup._id),
+      UserGroupRelation.findUserIdsByGroupId(parent._id),
+    ]);
+    const usersBelongsToTargetButNotParent = excludeTestIdsFromTargetIds(
+      targetGroupUsers,
+      parentGroupUsers,
     );
-    const usersBelongsToTargetButNotParent = excludeTestIdsFromTargetIds(targetGroupUsers, parentGroupUsers);
 
     // save if no users exist in both target and parent groups
     if (targetGroupUsers.length === 0 && parentGroupUsers.length === 0) {
@@ -108,16 +146,22 @@ class UserGroupService implements IUserGroupService {
 
     // add the target group's users to all ancestors
     if (forceUpdateParents) {
-      const ancestorGroups = await UserGroup.findGroupsWithAncestorsRecursively(parent);
-      const ancestorGroupIds = ancestorGroups.map(group => group._id);
-
-      await UserGroupRelation.createByGroupIdsAndUserIds(ancestorGroupIds, usersBelongsToTargetButNotParent);
+      const ancestorGroups =
+        await UserGroup.findGroupsWithAncestorsRecursively(parent);
+      const ancestorGroupIds = ancestorGroups.map((group) => group._id);
+
+      await UserGroupRelation.createByGroupIdsAndUserIds(
+        ancestorGroupIds,
+        usersBelongsToTargetButNotParent,
+      );
     }
     // throw if any of users in the target group is NOT included in the parent group
     else {
       const isUpdatable = usersBelongsToTargetButNotParent.length === 0;
       if (!isUpdatable) {
-        throw Error('The parent group does not contain the users in this group.');
+        throw Error(
+          'The parent group does not contain the users in this group.',
+        );
       }
     }
 
@@ -126,28 +170,45 @@ class UserGroupService implements IUserGroupService {
   }
 
   async removeCompletelyByRootGroupId(
-      deleteRootGroupId, action: PageActionOnGroupDelete, user, transferToUserGroup?: IGrantedGroup,
-      userGroupModel: Model<UserGroupDocument> & UserGroupModel = UserGroup,
-      userGroupRelationModel: Model<UserGroupRelationDocument> & UserGroupRelationModel = UserGroupRelation,
+    deleteRootGroupId,
+    action: PageActionOnGroupDelete,
+    user,
+    transferToUserGroup?: IGrantedGroup,
+    userGroupModel: Model<UserGroupDocument> & UserGroupModel = UserGroup,
+    userGroupRelationModel: Model<UserGroupRelationDocument> &
+      UserGroupRelationModel = UserGroupRelation,
   ): Promise<DeleteResult> {
     const rootGroup = await userGroupModel.findById(deleteRootGroupId);
     if (rootGroup == null) {
-      throw new Error(`UserGroup data does not exist. id: ${deleteRootGroupId}`);
+      throw new Error(
+        `UserGroup data does not exist. id: ${deleteRootGroupId}`,
+      );
     }
 
-    const groupsToDelete = await userGroupModel.findGroupsWithDescendantsRecursively([rootGroup]);
+    const groupsToDelete =
+      await userGroupModel.findGroupsWithDescendantsRecursively([rootGroup]);
 
     // 1. update page & remove all groups
-    await this.crowi.pageService.handlePrivatePagesForGroupsToDelete(groupsToDelete, action, transferToUserGroup, user);
+    await this.crowi.pageService.handlePrivatePagesForGroupsToDelete(
+      groupsToDelete,
+      action,
+      transferToUserGroup,
+      user,
+    );
     // 2. remove all groups
-    const deletedGroups = await userGroupModel.deleteMany({ _id: { $in: groupsToDelete.map(g => g._id) } });
+    const deletedGroups = await userGroupModel.deleteMany({
+      _id: { $in: groupsToDelete.map((g) => g._id) },
+    });
     // 3. remove all relations
     await userGroupRelationModel.removeAllByUserGroups(groupsToDelete);
 
     return deletedGroups;
   }
 
-  async removeUserByUsername(userGroupId: ObjectIdLike, username: string): Promise<{user: IUser, deletedGroupsCount: number}> {
+  async removeUserByUsername(
+    userGroupId: ObjectIdLike,
+    username: string,
+  ): Promise<{ user: IUser; deletedGroupsCount: number }> {
     const User = mongoose.model<IUser, { findUserByUsername }>('User');
 
     const [userGroup, user] = await Promise.all([
@@ -155,14 +216,19 @@ class UserGroupService implements IUserGroupService {
       User.findUserByUsername(username),
     ]);
 
-    const groupsOfRelationsToDelete = userGroup != null ? await UserGroup.findGroupsWithDescendantsRecursively([userGroup]) : [];
-    const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map(g => g._id);
+    const groupsOfRelationsToDelete =
+      userGroup != null
+        ? await UserGroup.findGroupsWithDescendantsRecursively([userGroup])
+        : [];
+    const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map((g) => g._id);
 
-    const deleteManyRes = await UserGroupRelation.deleteMany({ relatedUser: user._id, relatedGroup: { $in: relatedGroupIdsToDelete } });
+    const deleteManyRes = await UserGroupRelation.deleteMany({
+      relatedUser: user._id,
+      relatedGroup: { $in: relatedGroupIdsToDelete },
+    });
 
     return { user, deletedGroupsCount: deleteManyRes.deletedCount };
   }
-
 }
 
 export default UserGroupService;

+ 22 - 1
biome.json

@@ -31,7 +31,28 @@
       "!apps/app/src/client",
       "!apps/app/src/server/middlewares",
       "!apps/app/src/server/routes/apiv3/*.js",
-      "!apps/app/src/server/service"
+      "!apps/app/src/server/service/access-token",
+      "!apps/app/src/server/service/config-manager",
+      "!apps/app/src/server/service/file-uploader",
+      "!apps/app/src/server/service/global-notification",
+      "!apps/app/src/server/service/growi-bridge",
+      "!apps/app/src/server/service/growi-info",
+      "!apps/app/src/server/service/import",
+      "!apps/app/src/server/service/in-app-notification",
+      "!apps/app/src/server/service/interfaces",
+      "!apps/app/src/server/service/normalize-data",
+      "!apps/app/src/server/service/page",
+      "!apps/app/src/server/service/page-listing",
+      "!apps/app/src/server/service/revision",
+      "!apps/app/src/server/service/s2s-messaging",
+      "!apps/app/src/server/service/search-delegator",
+      "!apps/app/src/server/service/search-reconnect-context",
+      "!apps/app/src/server/service/slack-command-handler",
+      "!apps/app/src/server/service/slack-event-handler",
+      "!apps/app/src/server/service/socket-io",
+      "!apps/app/src/server/service/system-events",
+      "!apps/app/src/server/service/user-notification",
+      "!apps/app/src/server/service/yjs"
     ]
   },
   "formatter": {

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini