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

update IGrowiInfo interface to include serviceInstanceId and osInfo; modify legacy format conversion to handle appSiteUrlHashed

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

+ 5 - 1
apps/app/src/features/questionnaire/interfaces/growi-app-info.ts

@@ -11,4 +11,8 @@ export type IGrowiAppAdditionalInfo = IGrowiAdditionalInfo & {
 
 // legacy properties (extracted from additionalInfo for growi-questionnaire)
 // see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
-export type IGrowiAppInfoLegacy = Omit<IGrowiInfo<IGrowiAppAdditionalInfo>, 'additionalInfo'> & IGrowiAppAdditionalInfo;
+export type IGrowiAppInfoLegacy = Omit<IGrowiInfo<IGrowiAppAdditionalInfo>, 'additionalInfo'>
+  & IGrowiAppAdditionalInfo
+  & {
+    appSiteUrlHashed: string,
+  };

+ 1 - 1
apps/app/src/features/questionnaire/server/models/schema/growi-info.ts

@@ -19,7 +19,7 @@ const growiAdditionalInfoSchema = new Schema<IGrowiAppAdditionalInfo>({
 export const growiInfoSchema = new Schema<IGrowiInfo<IGrowiAppAdditionalInfo> & IGrowiAppAdditionalInfo>({
   version: { type: String, required: true },
   appSiteUrl: { type: String },
-  appSiteUrlHashed: { type: String, required: true },
+  serviceInstanceId: { type: String, required: true },
   type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
   wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
   osInfo: {

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

@@ -84,6 +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 isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
       const growiInfo = await getGrowiInfoService().getGrowiInfo(true);
       const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
 
@@ -98,7 +99,7 @@ module.exports = (crowi: Crowi): Router => {
         answeredAt: new Date(),
       };
 
-      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer);
+      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer, isAppSiteUrlHashed);
 
       try {
         await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
@@ -132,6 +133,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 isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
       const growiInfo = await getGrowiInfoService().getGrowiInfo(true);
       const userInfo = crowi.questionnaireService!.getUserInfo(user, growiInfo.appSiteUrlHashed);
 
@@ -143,7 +145,7 @@ module.exports = (crowi: Crowi): Router => {
         questionnaireOrder: req.body.questionnaireOrderId,
       };
 
-      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer);
+      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer, isAppSiteUrlHashed);
 
       try {
         await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswerLegacy);

+ 5 - 3
apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts

@@ -1,5 +1,6 @@
 import axiosRetry from 'axios-retry';
 
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import { getRandomIntInRange } from '~/utils/rand';
 
@@ -54,7 +55,8 @@ class QuestionnaireCronService {
   }
 
   async executeJob(): Promise<void> {
-    const questionnaireServerOrigin = this.crowi.configManager.getConfig('app:questionnaireServerOrigin');
+    const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
+    const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
 
     const fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
       const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
@@ -84,14 +86,14 @@ class QuestionnaireCronService {
 
       axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
         // convert to legacy format
-        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer)),
+        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
       })
         .then(async() => {
           await QuestionnaireAnswer.deleteMany();
         });
       axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
         // convert to legacy format
-        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer)),
+        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
       })
         .then(async() => {
           await ProactiveQuestionnaireAnswer.deleteMany();

+ 47 - 5
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts

@@ -1,7 +1,10 @@
 import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
 import type { IGrowiInfo } from '@growi/core/dist/interfaces';
 import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import { describe, test, expect } from 'vitest';
+import {
+  describe, test, expect,
+} from 'vitest';
+import { mock } from 'vitest-mock-extended';
 
 import { AttachmentMethodType } from '../../../../interfaces/attachment';
 import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
@@ -13,10 +16,17 @@ describe('convertToLegacyFormat', () => {
     const growiInfoLegacy: IGrowiAppInfoLegacy = {
       version: '1.0.0',
       appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: 'hashedUrl',
+      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
+      serviceInstanceId: 'service-instance-id',
       type: GrowiServiceType.cloud,
       wikiType: GrowiWikiType.open,
       deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
 
       // legacy properties
       installedAt: new Date(),
@@ -39,11 +49,16 @@ describe('convertToLegacyFormat', () => {
     const growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo> = {
       version: '1.0.0',
       appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: 'hashedUrl',
+      serviceInstanceId: 'service-instance-id',
       type: GrowiServiceType.cloud,
       wikiType: GrowiWikiType.open,
       deploymentType: GrowiDeploymentType.others,
-
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
       additionalInfo: {
         installedAt: new Date(),
         installedAtByOldestUser: new Date(),
@@ -60,10 +75,17 @@ describe('convertToLegacyFormat', () => {
     const growiInfoLegacy: IGrowiAppInfoLegacy = {
       version: '1.0.0',
       appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: 'hashedUrl',
+      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
+      serviceInstanceId: 'service-instance-id',
       type: GrowiServiceType.cloud,
       wikiType: GrowiWikiType.open,
       deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
 
       // legacy properties
       installedAt: new Date(),
@@ -80,4 +102,24 @@ describe('convertToLegacyFormat', () => {
     const result = convertToLegacyFormat(newFormatData);
     expect(result).toStrictEqual(expected);
   });
+
+  test('should convert new format and omit appSiteUrl', () => {
+    // arrange
+    const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
+      appSiteUrl: 'https://example.com',
+      additionalInfo: {
+        installedAt: new Date(),
+        installedAtByOldestUser: new Date(),
+        currentUsersCount: 1,
+        currentActiveUsersCount: 1,
+        attachmentType: AttachmentMethodType.local,
+      },
+    });
+
+    // act
+    const result = convertToLegacyFormat({ growiInfo }, true);
+
+    // assert
+    expect(result.growiInfo.appSiteUrl).toBeUndefined();
+  });
 });

+ 12 - 2
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts

@@ -1,4 +1,5 @@
 import assert from 'assert';
+import crypto from 'crypto';
 
 import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
 
@@ -12,18 +13,27 @@ function isLegacy<T extends { growiInfo: any }>(data: T): data is IHasGrowiAppIn
   return !('additionalInfo' in data.growiInfo);
 }
 
+function getSiteUrlHashed(_siteUrl: string | undefined): string {
+  const siteUrl = _siteUrl ?? '[The site URL is not set. Please set it!]';
+  const hasher = crypto.createHash('sha256');
+  hasher.update(siteUrl);
+  return hasher.digest('hex');
+}
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function convertToLegacyFormat<T extends { growiInfo: any }>(questionnaireAnswer: T): IHasGrowiAppInfoLegacy<T> {
+export function convertToLegacyFormat<T extends { growiInfo: any }>(questionnaireAnswer: T, isAppSiteUrlHashed = false): IHasGrowiAppInfoLegacy<T> {
   if (isLegacy(questionnaireAnswer)) {
     return questionnaireAnswer;
   }
 
-  const { additionalInfo, ...rest } = questionnaireAnswer.growiInfo;
+  const { additionalInfo, appSiteUrl, ...rest } = questionnaireAnswer.growiInfo;
   assert(additionalInfo != null);
 
   return {
     ...questionnaireAnswer,
     growiInfo: {
+      appSiteUrl: isAppSiteUrlHashed ? undefined : appSiteUrl,
+      appSiteUrlHashed: getSiteUrlHashed(appSiteUrl),
       ...rest,
       ...additionalInfo,
     },

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

@@ -27,7 +27,6 @@ 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 { serviceFactory as growiInfoServiceFactory } from '../service/growi-info';
 import { initializeImportService } from '../service/import';
 import { InstallerService } from '../service/installer';
 import { normalizeData } from '../service/normalize-data';
@@ -178,7 +177,6 @@ Crowi.prototype.init = async function() {
   ]);
 
   await Promise.all([
-    this.setupGrowiInfoService(),
     this.setupPassport(),
     this.setupSearcher(),
     this.setupMailer(),
@@ -646,13 +644,6 @@ Crowi.prototype.setUpApp = async function() {
   }
 };
 
-/**
- * setup GrowiInfoService
- */
-Crowi.prototype.setupGrowiInfoService = async function() {
-  growiInfoServiceFactory(this);
-};
-
 /**
  * setup FileUploadService
  */

+ 0 - 22
apps/app/src/server/service/app.ts

@@ -1,5 +1,4 @@
 import { ConfigSource } from '@growi/core/dist/interfaces';
-import { pathUtils } from '@growi/core/dist/utils';
 
 import loggerFactory from '~/utils/logger';
 
@@ -76,27 +75,6 @@ export default class AppService implements S2sMessageHandlable {
     return configManager.getConfig('app:title') ?? 'GROWI';
   }
 
-  /**
-   * get the site url
-   *
-   * If the config for the site url is not set, this returns a message "[The site URL is not set. Please set it!]".
-   *
-   * With version 3.2.3 and below, there is no config for the site URL, so the system always uses auto-generated site URL.
-   * With version 3.2.4 to 3.3.4, the system uses the auto-generated site URL only if the config is not set.
-   * With version 3.3.5 and above, the system use only a value from the config.
-   */
-  /* eslint-disable no-else-return */
-  getSiteUrl() {
-    const siteUrl = configManager.getConfig('app:siteUrl');
-    if (siteUrl != null) {
-      return pathUtils.removeTrailingSlash(siteUrl);
-    }
-    else {
-      return '[The site URL is not set. Please set it!]';
-    }
-  }
-  /* eslint-enable no-else-return */
-
   getTzoffset() {
     return -(configManager.getConfig('app:timezone') || 9) * 60;
   }

+ 21 - 25
apps/app/src/server/service/growi-info/growi-info.ts

@@ -4,11 +4,11 @@ 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 { pathUtils } from '@growi/core/dist/utils';
 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';
@@ -19,10 +19,21 @@ import type { IGrowiAppAdditionalInfo } from '../../../features/questionnaire/in
 
 export class GrowiInfoService {
 
-  crowi: Crowi;
-
-  constructor(crowi: Crowi) {
-    this.crowi = crowi;
+  /**
+   * get the site url
+   *
+   * If the config for the site url is not set, this returns a message "[The site URL is not set. Please set it!]".
+   *
+   * With version 3.2.3 and below, there is no config for the site URL, so the system always uses auto-generated site URL.
+   * With version 3.2.4 to 3.3.4, the system uses the auto-generated site URL only if the config is not set.
+   * With version 3.3.5 and above, the system use only a value from the config.
+   */
+  private getSiteUrl(): string | undefined {
+    const siteUrl = configManager.getConfig('app:siteUrl');
+    if (siteUrl != null) {
+      return pathUtils.removeTrailingSlash(siteUrl);
+    }
+    return siteUrl;
   }
 
   /**
@@ -38,15 +49,13 @@ export class GrowiInfoService {
 
   async getGrowiInfo(includeAdditionalInfo?: boolean): Promise<IGrowiInfo<Record<string, never>> | IGrowiInfo<IGrowiAppAdditionalInfo>> {
 
-    const appSiteUrl = this.crowi.appService.getSiteUrl();
-    const hasher = crypto.createHash('sha256');
-    hasher.update(appSiteUrl);
-    const appSiteUrlHashed = hasher.digest('hex');
+    const appSiteUrl = this.getSiteUrl();
 
     const isGuestAllowedToRead = aclService.isGuestAllowedToRead();
     const wikiType = isGuestAllowedToRead ? GrowiWikiType.open : GrowiWikiType.closed;
 
     const baseInfo = {
+      serviceInstanceId: configManager.getConfig('app:serviceInstanceId'),
       version: getGrowiVersion(),
       osInfo: {
         type: os.type(),
@@ -54,12 +63,11 @@ export class GrowiInfoService {
         arch: os.arch(),
         totalmem: os.totalmem(),
       },
-      appSiteUrl: configManager.getConfig('questionnaire:isAppSiteUrlHashed') ? undefined : appSiteUrl,
-      appSiteUrlHashed,
+      appSiteUrl,
       type: configManager.getConfig('app:serviceType'),
       wikiType,
       deploymentType: configManager.getConfig('app:deploymentType'),
-    };
+    } satisfies IGrowiInfo<Record<string, never>>;
 
     if (!includeAdditionalInfo) {
       return baseInfo;
@@ -101,16 +109,4 @@ export class GrowiInfoService {
 
 }
 
-let _instance: GrowiInfoService;
-
-export const serviceFactory = (crowi: Crowi): GrowiInfoService => {
-  if (_instance == null) {
-    _instance = new GrowiInfoService(crowi);
-  }
-
-  return _instance;
-};
-
-export const getInstance = (): GrowiInfoService => {
-  return _instance;
-};
+export const growiInfoService = new GrowiInfoService();

+ 30 - 4
apps/app/test/integration/service/questionnaire-cron.test.ts

@@ -279,10 +279,16 @@ describe('QuestionnaireCronService', () => {
       answeredAt: new Date(),
       growiInfo: {
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         additionalInfo: {
           installedAt: new Date('2000-01-01'),
           installedAtByOldestUser: new Date('2020-01-01'),
@@ -307,7 +313,8 @@ describe('QuestionnaireCronService', () => {
       answeredAt: new Date(),
       growiInfo: {
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        appSiteUrlHashed: 'hashed',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
@@ -315,6 +322,12 @@ describe('QuestionnaireCronService', () => {
         installedAtByOldestUser: new Date('2020-01-01'),
         currentUsersCount: 100,
         currentActiveUsersCount: 50,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         attachmentType: AttachmentMethodType.aws,
       },
       userInfo: {
@@ -338,10 +351,16 @@ describe('QuestionnaireCronService', () => {
       commentText: 'answer text',
       growiInfo: {
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         additionalInfo: {
           installedAt: new Date('2000-01-01'),
           installedAtByOldestUser: new Date('2020-01-01'),
@@ -362,10 +381,17 @@ describe('QuestionnaireCronService', () => {
       commentText: 'answer text',
       growiInfo: {
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        appSiteUrlHashed: 'hashed',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         // legacy properties
         installedAt: new Date('2000-01-01'),
         installedAtByOldestUser: new Date('2020-01-01'),

+ 3 - 3
packages/core/src/interfaces/growi-app-info.ts

@@ -20,12 +20,12 @@ export interface IGrowiAdditionalInfo {
 }
 
 export interface IGrowiInfo<A extends object = IGrowiAdditionalInfo> {
-  version: string
+  serviceInstanceId: string
   appSiteUrl?: string
-  appSiteUrlHashed: string
+  osInfo: IGrowiOSInfo
+  version: string
   type: GrowiServiceType
   wikiType: GrowiWikiType
   deploymentType: GrowiDeploymentType
-  osInfo?: IGrowiOSInfo
   additionalInfo?: A
 }