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

devide getGrowiInfo as a service

Yuki Takei 1 год назад
Родитель
Сommit
f0188e4f67

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

@@ -7,6 +7,7 @@ import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import { configManager } from '~/server/service/config-manager';
+import { getInstance as getGrowiInfoService } from '~/server/service/growi-info';
 import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 
@@ -61,7 +62,7 @@ module.exports = (crowi: Crowi): Router => {
   };
 
   router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
+    const growiInfo = await getGrowiInfoService().getGrowiInfo();
     const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
 
     try {
@@ -83,7 +84,7 @@ module.exports = (crowi: Crowi): Router => {
   router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const sendQuestionnaireAnswer = async() => {
       const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
+      const growiInfo = await getGrowiInfoService().getGrowiInfo();
       const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
 
       const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
@@ -131,7 +132,7 @@ module.exports = (crowi: Crowi): Router => {
   router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
       const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
+      const growiInfo = await getGrowiInfoService().getGrowiInfo();
       const userInfo = crowi.questionnaireService!.getUserInfo(user, growiInfo.appSiteUrlHashed);
 
       const questionnaireAnswer: IQuestionnaireAnswer = {

+ 11 - 69
apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts

@@ -1,14 +1,14 @@
-import { Types } from 'mongoose';
+import type { IGrowiInfo } from '^/../../packages/core/dist';
 import { mock } from 'vitest-mock-extended';
 
 import pkg from '^/package.json';
 
 
 import type UserEvent from '~/server/events/user';
-import { Config } from '~/server/models/config';
 import { configManager } from '~/server/service/config-manager';
 
 import type Crowi from '../../../../server/crowi';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { UserType } from '../../interfaces/user-info';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
@@ -26,21 +26,8 @@ describe('QuestionnaireService', () => {
   let user;
 
   beforeAll(async() => {
-    process.env.APP_SITE_URL = 'http://growi.test.jp';
-    process.env.DEPLOYMENT_TYPE = 'growi-docker-compose';
-    process.env.SAML_ENABLED = 'true';
 
     await configManager.loadConfigs();
-    await configManager.updateConfigs({
-      'security:passport-saml:isEnabled': true,
-      'security:passport-github:isEnabled': true,
-    });
-
-    await Config.create({
-      key: 'app:installed',
-      value: true,
-      createdAt: '2000-01-01',
-    });
 
     const crowiMock = mock<Crowi>({
       version: appVersion,
@@ -51,13 +38,12 @@ describe('QuestionnaireService', () => {
           });
         }
       }),
-      appService: {
-        getSiteUrl: () => 'http://growi.test.jp',
-      },
+      // appService: {
+      //   getSiteUrl: () => 'http://growi.test.jp',
+      // },
     });
     const userModelFactory = (await import('~/server/models/user')).default;
     User = userModelFactory(crowiMock);
-    questionnaireService = new QuestionnaireService(crowiMock);
 
     await User.deleteMany({}); // clear users
     user = await User.create({
@@ -67,55 +53,8 @@ describe('QuestionnaireService', () => {
       password: 'usertestpass',
       createdAt: '2000-01-01',
     });
-  });
-
-  describe('getGrowiInfo', () => {
-    test('Should get correct GROWI info', async() => {
-      const growiInfo = await questionnaireService.getGrowiInfo();
 
-      assert(growiInfo != null);
-
-      expect(growiInfo.appSiteUrlHashed).toBeTruthy();
-      expect(growiInfo.appSiteUrlHashed).not.toBe('http://growi.test.jp');
-      expect(growiInfo.osInfo?.type).toBeTruthy();
-      expect(growiInfo.osInfo?.platform).toBeTruthy();
-      expect(growiInfo.osInfo?.arch).toBeTruthy();
-      expect(growiInfo.osInfo?.totalmem).toBeTruthy();
-
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      delete (growiInfo as any).appSiteUrlHashed;
-      delete growiInfo.osInfo;
-
-      expect(growiInfo).toEqual({
-        version: appVersion,
-        appSiteUrl: 'http://growi.test.jp',
-        type: 'on-premise',
-        wikiType: 'closed',
-        deploymentType: 'growi-docker-compose',
-        additionalInfo: {
-          installedAt: new Date('2000-01-01'),
-          installedAtByOldestUser: new Date('2000-01-01'),
-          currentUsersCount: 1,
-          currentActiveUsersCount: 1,
-          attachmentType: 'aws',
-          activeExternalAccountTypes: ['saml', 'github'],
-        },
-      });
-    });
-
-    describe('When url hash settings is on', () => {
-      beforeEach(async() => {
-        process.env.QUESTIONNAIRE_IS_APP_SITE_URL_HASHED = 'true';
-        await configManager.loadConfigs();
-      });
-
-      test('Should return app url string', async() => {
-        const growiInfo = await questionnaireService.getGrowiInfo();
-        expect(growiInfo.appSiteUrl).toBeUndefined();
-        expect(growiInfo.appSiteUrlHashed).not.toBe('http://growi.test.jp');
-        expect(growiInfo.appSiteUrlHashed).toBeTruthy();
-      });
-    });
+    questionnaireService = new QuestionnaireService(crowiMock);
   });
 
   describe('getUserInfo', () => {
@@ -318,8 +257,11 @@ describe('QuestionnaireService', () => {
     });
 
     test('Should get questionnaire orders to show', async() => {
-      const growiInfo = await questionnaireService.getGrowiInfo();
-      const userInfo = questionnaireService.getUserInfo(user, growiInfo.appSiteUrlHashed);
+      const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
+        type: 'on-premise',
+        version: appVersion,
+      });
+      const userInfo = questionnaireService.getUserInfo(user, 'appSiteUrlHashed');
 
       const questionnaireOrderDocuments = await questionnaireService.getQuestionnaireOrdersToShow(userInfo, growiInfo, user._id);
 

+ 1 - 68
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -1,19 +1,10 @@
 import crypto from 'crypto';
-import * as os from 'node:os';
-
 
 import type { IUserHasId } from '@growi/core';
-import type { IGrowiInfo, IUser } from '@growi/core/dist/interfaces';
-import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import type { Model } from 'mongoose';
-import mongoose from 'mongoose';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
 
-import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import type Crowi from '~/server/crowi';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import { Config } from '~/server/models/config';
-import { aclService } from '~/server/service/acl';
-import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
 import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
@@ -36,64 +27,6 @@ class QuestionnaireService {
     this.crowi = crowi;
   }
 
-  async getGrowiInfo(): Promise<IGrowiInfo<IGrowiAppAdditionalInfo>> {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const User = mongoose.model<IUser, Model<IUser>>('User');
-
-    const appSiteUrl = this.crowi.appService.getSiteUrl();
-    const hasher = crypto.createHash('sha256');
-    hasher.update(appSiteUrl);
-    const appSiteUrlHashed = hasher.digest('hex');
-
-    // Get the oldest user who probably installed this GROWI.
-    // https://mongoosejs.com/docs/6.x/docs/api.html#model_Model-findOne
-    // https://stackoverflow.com/questions/13443069/mongoose-findone-with-sorting
-    const user = await User.findOne({ createdAt: { $ne: null } }).sort({ createdAt: 1 });
-
-    const installedAtByOldestUser = user ? user.createdAt : null;
-
-    const appInstalledConfig = await Config.findOne({ key: 'app:installed' });
-    const oldestConfig = await Config.findOne().sort({ createdAt: 1 });
-
-    // oldestConfig must not be null because there is at least one config
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const installedAt = installedAtByOldestUser ?? appInstalledConfig?.createdAt ?? oldestConfig!.createdAt ?? null;
-
-    const currentUsersCount = await User.countDocuments();
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const currentActiveUsersCount = await (User as any).countActiveUsers();
-
-    const isGuestAllowedToRead = aclService.isGuestAllowedToRead();
-    const wikiType = isGuestAllowedToRead ? GrowiWikiType.open : GrowiWikiType.closed;
-
-    const activeExternalAccountTypes: IExternalAuthProviderType[] = Object.values(IExternalAuthProviderType).filter((type) => {
-      return configManager.getConfig(`security:passport-${type}:isEnabled`);
-    });
-
-    return {
-      version: this.crowi.version,
-      osInfo: {
-        type: os.type(),
-        platform: os.platform(),
-        arch: os.arch(),
-        totalmem: os.totalmem(),
-      },
-      appSiteUrl: configManager.getConfig('questionnaire:isAppSiteUrlHashed') ? undefined : appSiteUrl,
-      appSiteUrlHashed,
-      type: configManager.getConfig('app:serviceType'),
-      wikiType,
-      deploymentType: configManager.getConfig('app:deploymentType'),
-      additionalInfo: {
-        installedAt,
-        installedAtByOldestUser,
-        currentUsersCount,
-        currentActiveUsersCount,
-        attachmentType: configManager.getConfig('app:fileUploadType'),
-        activeExternalAccountTypes,
-      },
-    };
-  }
-
   getUserInfo(user: IUserHasId | null, appSiteUrlHashed: string): IUserInfo {
     if (user != null) {
       const hasher = crypto.createHmac('sha256', appSiteUrlHashed);

+ 12 - 0
apps/app/src/server/crowi/index.js

@@ -27,6 +27,7 @@ import { configManager as configManagerSingletonInstance } from '../service/conf
 import { instanciate as instanciateExternalAccountService } from '../service/external-account';
 import { FileUploader, getUploader } from '../service/file-uploader'; // eslint-disable-line no-unused-vars
 import { G2GTransferPusherService, G2GTransferReceiverService } from '../service/g2g-transfer';
+import { GrowiInfoService } from '../service/growi-info';
 import { initializeImportService } from '../service/import';
 import { InstallerService } from '../service/installer';
 import { normalizeData } from '../service/normalize-data';
@@ -70,6 +71,9 @@ class Crowi {
   /** @type {FileUploader} */
   fileUploadService;
 
+  /** @type {GrowiInfoService} */
+  growiInfoService;
+
   /** @type {import('../service/page').IPageService} */
   pageService;
 
@@ -177,6 +181,7 @@ Crowi.prototype.init = async function() {
   ]);
 
   await Promise.all([
+    this.setupGrowiInfoService(),
     this.setupPassport(),
     this.setupSearcher(),
     this.setupMailer(),
@@ -638,6 +643,13 @@ Crowi.prototype.setUpApp = async function() {
   }
 };
 
+/**
+ * setup GrowiInfoService
+ */
+Crowi.prototype.setupGrowiInfoService = async function() {
+  this.growiInfoService = new GrowiInfoService(this);
+};
+
 /**
  * setup FileUploadService
  */

+ 112 - 0
apps/app/src/server/service/growi-info/growi-info.integ.ts

@@ -0,0 +1,112 @@
+import { mock } from 'vitest-mock-extended';
+
+import pkg from '^/package.json';
+
+import type UserEvent from '~/server/events/user';
+import { Config } from '~/server/models/config';
+import { configManager } from '~/server/service/config-manager';
+
+import type Crowi from '../../crowi';
+
+import { GrowiInfoService } from './growi-info';
+
+describe('GrowiInfoService', () => {
+  const appVersion = pkg.version;
+
+  let growiInfoService: GrowiInfoService;
+  let User;
+  let user;
+
+  beforeAll(async() => {
+    process.env.APP_SITE_URL = 'http://growi.test.jp';
+    process.env.DEPLOYMENT_TYPE = 'growi-docker-compose';
+    process.env.SAML_ENABLED = 'true';
+
+    await configManager.loadConfigs();
+    await configManager.updateConfigs({
+      'security:passport-saml:isEnabled': true,
+      'security:passport-github:isEnabled': true,
+    });
+
+    await Config.create({
+      key: 'app:installed',
+      value: true,
+      createdAt: '2000-01-01',
+    });
+
+    const crowiMock = mock<Crowi>({
+      version: appVersion,
+      event: vi.fn().mockImplementation((eventName) => {
+        if (eventName === 'user') {
+          return mock<UserEvent>({
+            on: vi.fn(),
+          });
+        }
+      }),
+      appService: {
+        getSiteUrl: () => 'http://growi.test.jp',
+      },
+    });
+
+    growiInfoService = new GrowiInfoService(crowiMock);
+
+    const userModelFactory = (await import('~/server/models/user')).default;
+    User = userModelFactory(crowiMock);
+
+    await User.deleteMany({}); // clear users
+
+    user = await User.create({
+      username: 'growiinfo test user',
+      createdAt: '2000-01-01',
+    });
+  });
+
+  describe('getGrowiInfo', () => {
+    test('Should get correct GROWI info', async() => {
+      const growiInfo = await growiInfoService.getGrowiInfo();
+
+      assert(growiInfo != null);
+
+      expect(growiInfo.appSiteUrlHashed).toBeTruthy();
+      expect(growiInfo.appSiteUrlHashed).not.toBe('http://growi.test.jp');
+      expect(growiInfo.osInfo?.type).toBeTruthy();
+      expect(growiInfo.osInfo?.platform).toBeTruthy();
+      expect(growiInfo.osInfo?.arch).toBeTruthy();
+      expect(growiInfo.osInfo?.totalmem).toBeTruthy();
+
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      delete (growiInfo as any).appSiteUrlHashed;
+      delete growiInfo.osInfo;
+
+      expect(growiInfo).toEqual({
+        version: appVersion,
+        appSiteUrl: 'http://growi.test.jp',
+        type: 'on-premise',
+        wikiType: 'closed',
+        deploymentType: 'growi-docker-compose',
+        additionalInfo: {
+          installedAt: new Date('2000-01-01'),
+          installedAtByOldestUser: new Date('2000-01-01'),
+          currentUsersCount: 1,
+          currentActiveUsersCount: 1,
+          attachmentType: 'aws',
+          activeExternalAccountTypes: ['saml', 'github'],
+        },
+      });
+    });
+
+    describe('When url hash settings is on', () => {
+      beforeEach(async() => {
+        process.env.QUESTIONNAIRE_IS_APP_SITE_URL_HASHED = 'true';
+        await configManager.loadConfigs();
+      });
+
+      test('Should return app url string', async() => {
+        const growiInfo = await growiInfoService.getGrowiInfo();
+        expect(growiInfo.appSiteUrl).toBeUndefined();
+        expect(growiInfo.appSiteUrlHashed).not.toBe('http://growi.test.jp');
+        expect(growiInfo.appSiteUrlHashed).toBeTruthy();
+      });
+    });
+  });
+});

+ 97 - 0
apps/app/src/server/service/growi-info/growi-info.ts

@@ -0,0 +1,97 @@
+import crypto from 'crypto';
+import * as os from 'node:os';
+
+import type { IGrowiInfo } from '@growi/core';
+import type { IUser } from '@growi/core/dist/interfaces';
+import { GrowiWikiType } from '@growi/core/dist/interfaces';
+import type { Model } from 'mongoose';
+import mongoose from 'mongoose';
+
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
+import type Crowi from '~/server/crowi';
+import { Config } from '~/server/models/config';
+import { aclService } from '~/server/service/acl';
+import { configManager } from '~/server/service/config-manager';
+
+import type { IGrowiAppAdditionalInfo } from '../../../features/questionnaire/interfaces/growi-app-info';
+
+
+export class GrowiInfoService {
+
+  crowi: Crowi;
+
+  constructor(crowi: Crowi) {
+    this.crowi = crowi;
+  }
+
+  async getGrowiInfo(): Promise<IGrowiInfo<IGrowiAppAdditionalInfo>> {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const User = mongoose.model<IUser, Model<IUser>>('User');
+
+    const appSiteUrl = this.crowi.appService.getSiteUrl();
+    const hasher = crypto.createHash('sha256');
+    hasher.update(appSiteUrl);
+    const appSiteUrlHashed = hasher.digest('hex');
+
+    // Get the oldest user who probably installed this GROWI.
+    const user = await User.findOne({ createdAt: { $ne: null } }).sort({ createdAt: 1 });
+
+    const installedAtByOldestUser = user ? user.createdAt : null;
+
+    const appInstalledConfig = await Config.findOne({ key: 'app:installed' });
+    const oldestConfig = await Config.findOne().sort({ createdAt: 1 });
+
+    // oldestConfig must not be null because there is at least one config
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const installedAt = installedAtByOldestUser ?? appInstalledConfig?.createdAt ?? oldestConfig!.createdAt ?? null;
+
+    const currentUsersCount = await User.countDocuments();
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const currentActiveUsersCount = await (User as any).countActiveUsers();
+
+    const isGuestAllowedToRead = aclService.isGuestAllowedToRead();
+    const wikiType = isGuestAllowedToRead ? GrowiWikiType.open : GrowiWikiType.closed;
+
+    const activeExternalAccountTypes: IExternalAuthProviderType[] = Object.values(IExternalAuthProviderType).filter((type) => {
+      return configManager.getConfig(`security:passport-${type}:isEnabled`);
+    });
+
+    return {
+      version: this.crowi.version,
+      osInfo: {
+        type: os.type(),
+        platform: os.platform(),
+        arch: os.arch(),
+        totalmem: os.totalmem(),
+      },
+      appSiteUrl: configManager.getConfig('questionnaire:isAppSiteUrlHashed') ? undefined : appSiteUrl,
+      appSiteUrlHashed,
+      type: configManager.getConfig('app:serviceType'),
+      wikiType,
+      deploymentType: configManager.getConfig('app:deploymentType'),
+      additionalInfo: {
+        installedAt,
+        installedAtByOldestUser,
+        currentUsersCount,
+        currentActiveUsersCount,
+        attachmentType: configManager.getConfig('app:fileUploadType'),
+        activeExternalAccountTypes,
+      },
+    };
+  }
+
+}
+
+let _instance: GrowiInfoService;
+
+export const serviceFactory = (crowi: Crowi): GrowiInfoService => {
+  if (_instance == null) {
+    _instance = new GrowiInfoService(crowi);
+  }
+
+  return _instance;
+};
+
+export const getInstance = (): GrowiInfoService => {
+  return _instance;
+};

+ 1 - 0
apps/app/src/server/service/growi-info/index.ts

@@ -0,0 +1 @@
+export * from './growi-info';