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

Merge pull request #9542 from weseek/imprv/generate-site-url-hashed-isolatedly

imprv: Set the service instance id after DB is initialized
Yuki Takei 1 год назад
Родитель
Сommit
d0bed9b30d
65 измененных файлов с 1086 добавлено и 663 удалено
  1. 5 0
      .changeset/clever-impalas-dress.md
  2. 9 9
      apps/app/package.json
  3. 1 1
      apps/app/src/features/opentelemetry/server/index.ts
  4. 43 22
      apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts
  5. 17 6
      apps/app/src/features/opentelemetry/server/node-sdk.ts
  6. 5 1
      apps/app/src/features/questionnaire/interfaces/growi-app-info.ts
  7. 1 1
      apps/app/src/features/questionnaire/server/models/schema/growi-info.ts
  8. 12 9
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  9. 5 3
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts
  10. 11 69
      apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts
  11. 1 68
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  12. 47 5
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts
  13. 11 2
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts
  14. 24 0
      apps/app/src/migrations/19700101000000-foremost-1010-20250109000000-generate-service-instance-id.js
  15. 2 2
      apps/app/src/pages/admin/security.page.tsx
  16. 2 2
      apps/app/src/pages/admin/slack-integration.page.tsx
  17. 2 1
      apps/app/src/pages/utils/commons.ts
  18. 4 4
      apps/app/src/server/app.ts
  19. 2 1
      apps/app/src/server/crowi/express-init.js
  20. 11 3
      apps/app/src/server/crowi/index.js
  21. 1 1
      apps/app/src/server/models/obsolete-page.js
  22. 5 2
      apps/app/src/server/models/slack-app-integration.js
  23. 2 1
      apps/app/src/server/routes/apiv3/admin-home.ts
  24. 1 0
      apps/app/src/server/routes/apiv3/attachment.js
  25. 2 1
      apps/app/src/server/routes/apiv3/forgot-password.js
  26. 1 1
      apps/app/src/server/routes/apiv3/import.js
  27. 1 0
      apps/app/src/server/routes/apiv3/index.js
  28. 1 0
      apps/app/src/server/routes/apiv3/markdown-setting.js
  29. 1 0
      apps/app/src/server/routes/apiv3/notification-setting.js
  30. 1 1
      apps/app/src/server/routes/apiv3/pages/index.js
  31. 1 0
      apps/app/src/server/routes/apiv3/personal-setting.js
  32. 1 0
      apps/app/src/server/routes/apiv3/revisions.js
  33. 1 0
      apps/app/src/server/routes/apiv3/share-links.js
  34. 1 0
      apps/app/src/server/routes/apiv3/slack-integration-legacy-settings.js
  35. 1 1
      apps/app/src/server/routes/apiv3/slack-integration-settings.js
  36. 5 4
      apps/app/src/server/routes/apiv3/slack-integration.js
  37. 1 0
      apps/app/src/server/routes/apiv3/staffs.js
  38. 3 2
      apps/app/src/server/routes/apiv3/user-activation.ts
  39. 4 2
      apps/app/src/server/routes/apiv3/users.js
  40. 4 1
      apps/app/src/server/routes/login.js
  41. 0 22
      apps/app/src/server/service/app.ts
  42. 79 0
      apps/app/src/server/service/config-manager/config-definition.ts
  43. 9 0
      apps/app/src/server/service/config-manager/config-manager.ts
  44. 4 2
      apps/app/src/server/service/export.js
  45. 4 2
      apps/app/src/server/service/g2g-transfer.ts
  46. 2 1
      apps/app/src/server/service/global-notification/global-notification-slack.js
  47. 7 3
      apps/app/src/server/service/global-notification/index.js
  48. 117 0
      apps/app/src/server/service/growi-info/growi-info.integ.ts
  49. 111 0
      apps/app/src/server/service/growi-info/growi-info.ts
  50. 1 0
      apps/app/src/server/service/growi-info/index.ts
  51. 2 1
      apps/app/src/server/service/import/import.ts
  52. 4 2
      apps/app/src/server/service/mail.ts
  53. 28 22
      apps/app/src/server/service/passport.ts
  54. 5 4
      apps/app/src/server/service/s2s-messaging/index.ts
  55. 0 2
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  56. 7 1
      apps/app/src/server/service/slack-command-handler/create-page-service.js
  57. 4 1
      apps/app/src/server/service/slack-command-handler/help.js
  58. 4 2
      apps/app/src/server/service/slack-command-handler/search.js
  59. 4 3
      apps/app/src/server/service/slack-event-handler/link-shared.ts
  60. 2 1
      apps/app/src/server/service/user-notification/index.ts
  61. 2 0
      apps/app/src/server/util/importer.js
  62. 5 0
      apps/app/src/utils/growi-version.ts
  63. 35 4
      apps/app/test/integration/service/questionnaire-cron.test.ts
  64. 4 4
      packages/core/src/interfaces/growi-app-info.ts
  65. 393 360
      pnpm-lock.yaml

+ 5 - 0
.changeset/clever-impalas-dress.md

@@ -0,0 +1,5 @@
+---
+'@growi/core': minor
+---
+
+Update GrowiInfo interface

+ 9 - 9
apps/app/package.json

@@ -83,15 +83,15 @@
     "@growi/remark-lsx": "workspace:^",
     "@growi/remark-lsx": "workspace:^",
     "@growi/slack": "workspace:^",
     "@growi/slack": "workspace:^",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@keycloak/keycloak-admin-client": "^18.0.0",
-    "@opentelemetry/api": "^1.8.0",
-    "@opentelemetry/auto-instrumentations-node": "^0.52.1",
-    "@opentelemetry/exporter-metrics-otlp-grpc": "^0.54.2",
-    "@opentelemetry/exporter-trace-otlp-grpc": "^0.54.2",
-    "@opentelemetry/resources": "^1.27.0",
-    "@opentelemetry/semantic-conventions": "^1.27.0",
-    "@opentelemetry/sdk-metrics": "^1.27.0",
-    "@opentelemetry/sdk-node": "^0.54.2",
-    "@opentelemetry/sdk-trace-node": "^1.27.0",
+    "@opentelemetry/api": "^1.9.0",
+    "@opentelemetry/auto-instrumentations-node": "^0.55.1",
+    "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.0",
+    "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
+    "@opentelemetry/resources": "^1.28.0",
+    "@opentelemetry/semantic-conventions": "^1.28.0",
+    "@opentelemetry/sdk-metrics": "^1.28.0",
+    "@opentelemetry/sdk-node": "^0.57.0",
+    "@opentelemetry/sdk-trace-node": "^1.28.0",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
     "JSONStream": "^1.3.5",
     "JSONStream": "^1.3.5",

+ 1 - 1
apps/app/src/features/opentelemetry/server/index.ts

@@ -1 +1 @@
-export * from './start';
+export * from './node-sdk';

+ 43 - 22
apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts

@@ -1,35 +1,56 @@
 import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
 import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
 import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
 import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
 import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
 import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
-import { Resource } from '@opentelemetry/resources';
+import { Resource, type IResource } from '@opentelemetry/resources';
 import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
 import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
 import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
 import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
 import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_INSTANCE_ID } from '@opentelemetry/semantic-conventions';
 import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_INSTANCE_ID } from '@opentelemetry/semantic-conventions';
 
 
+import { getGrowiVersion } from '~/utils/growi-version';
 
 
-export const generateNodeSDKConfiguration = (instanceId: string, version: string): Partial<NodeSDKConfiguration> => {
-  return {
-    resource: new Resource({
+type Configuration = Partial<NodeSDKConfiguration> & {
+  resource: IResource;
+};
+
+let resource: Resource;
+let configuration: Configuration;
+
+export const generateNodeSDKConfiguration = (serviceInstanceId?: string): Configuration => {
+  if (configuration == null) {
+    const version = getGrowiVersion();
+
+    resource = new Resource({
       [ATTR_SERVICE_NAME]: 'growi',
       [ATTR_SERVICE_NAME]: 'growi',
       [ATTR_SERVICE_VERSION]: version,
       [ATTR_SERVICE_VERSION]: version,
-      [SEMRESATTRS_SERVICE_INSTANCE_ID]: instanceId,
-    }),
-    traceExporter: new OTLPTraceExporter(),
-    metricReader: new PeriodicExportingMetricReader({
-      exporter: new OTLPMetricExporter(),
-      exportIntervalMillis: 10000,
-    }),
-    instrumentations: [getNodeAutoInstrumentations({
-      '@opentelemetry/instrumentation-bunyan': {
-        enabled: false,
-      },
-      // disable fs instrumentation since this generates very large amount of traces
-      // see: https://opentelemetry.io/docs/languages/js/libraries/#registration
-      '@opentelemetry/instrumentation-fs': {
-        enabled: false,
-      },
-    })],
-  };
+    });
+
+    configuration = {
+      resource,
+      traceExporter: new OTLPTraceExporter(),
+      metricReader: new PeriodicExportingMetricReader({
+        exporter: new OTLPMetricExporter(),
+        exportIntervalMillis: 10000,
+      }),
+      instrumentations: [getNodeAutoInstrumentations({
+        '@opentelemetry/instrumentation-bunyan': {
+          enabled: false,
+        },
+        // disable fs instrumentation since this generates very large amount of traces
+        // see: https://opentelemetry.io/docs/languages/js/libraries/#registration
+        '@opentelemetry/instrumentation-fs': {
+          enabled: false,
+        },
+      })],
+    };
+  }
+
+  if (serviceInstanceId != null) {
+    configuration.resource = resource.merge(new Resource({
+      [SEMRESATTRS_SERVICE_INSTANCE_ID]: serviceInstanceId,
+    }));
+  }
+
+  return configuration;
 };
 };
 
 
 // public async shutdownInstrumentation(): Promise<void> {
 // public async shutdownInstrumentation(): Promise<void> {

+ 17 - 6
apps/app/src/features/opentelemetry/server/start.ts → apps/app/src/features/opentelemetry/server/node-sdk.ts

@@ -4,7 +4,6 @@ import type { NodeSDK } from '@opentelemetry/sdk-node';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 const logger = loggerFactory('growi:opentelemetry:server');
 const logger = loggerFactory('growi:opentelemetry:server');
 
 
 
 
@@ -37,7 +36,7 @@ function overwriteSdkDisabled(): void {
 
 
 }
 }
 
 
-export const startInstrumentation = async(version: string): Promise<void> => {
+export const startInstrumentation = async(): Promise<void> => {
   if (sdkInstance != null) {
   if (sdkInstance != null) {
     logger.warn('OpenTelemetry instrumentation already started');
     logger.warn('OpenTelemetry instrumentation already started');
     return;
     return;
@@ -69,14 +68,26 @@ For more information, see https://docs.growi.org/en/admin-guide/telemetry.html.
     const { NodeSDK } = await import('@opentelemetry/sdk-node');
     const { NodeSDK } = await import('@opentelemetry/sdk-node');
     const { generateNodeSDKConfiguration } = await import('./node-sdk-configuration');
     const { generateNodeSDKConfiguration } = await import('./node-sdk-configuration');
 
 
-    const serviceInstanceId = configManager.getConfig('otel:serviceInstanceId', ConfigSource.env)
-      ?? 'generated-appSiteUrlHashed'; // TODO: generated appSiteUrlHashed
-
-    sdkInstance = new NodeSDK(generateNodeSDKConfiguration(serviceInstanceId, version));
+    sdkInstance = new NodeSDK(generateNodeSDKConfiguration());
     sdkInstance.start();
     sdkInstance.start();
   }
   }
 };
 };
 
 
+export const initServiceInstanceId = async(): Promise<void> => {
+  const instrumentationEnabled = configManager.getConfig('otel:enabled', ConfigSource.env);
+
+  if (instrumentationEnabled) {
+    const { generateNodeSDKConfiguration } = await import('./node-sdk-configuration');
+
+    const serviceInstanceId = configManager.getConfig('otel:serviceInstanceId')
+      ?? configManager.getConfig('app:serviceInstanceId');
+
+    // overwrite resource
+    const updatedResource = generateNodeSDKConfiguration(serviceInstanceId).resource;
+    (sdkInstance as any).resource = updatedResource;
+  }
+};
+
 // public async shutdownInstrumentation(): Promise<void> {
 // public async shutdownInstrumentation(): Promise<void> {
 //   await this.sdkInstance.shutdown();
 //   await this.sdkInstance.shutdown();
 
 

+ 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)
 // legacy properties (extracted from additionalInfo for growi-questionnaire)
 // see: https://gitlab.weseek.co.jp/tech/growi/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>({
 export const growiInfoSchema = new Schema<IGrowiInfo<IGrowiAppAdditionalInfo> & IGrowiAppAdditionalInfo>({
   version: { type: String, required: true },
   version: { type: String, required: true },
   appSiteUrl: { type: String },
   appSiteUrl: { type: String },
-  appSiteUrlHashed: { type: String, required: true },
+  serviceInstanceId: { type: String, required: true },
   type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
   type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
   wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
   wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
   osInfo: {
   osInfo: {

+ 12 - 9
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 { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { growiInfoService } from '~/server/service/growi-info';
 import axios from '~/utils/axios';
 import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -17,7 +18,7 @@ import { StatusType } from '../../../interfaces/questionnaire-answer-status';
 import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
 import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
 import QuestionnaireAnswer from '../../models/questionnaire-answer';
 import QuestionnaireAnswer from '../../models/questionnaire-answer';
 import QuestionnaireAnswerStatus from '../../models/questionnaire-answer-status';
 import QuestionnaireAnswerStatus from '../../models/questionnaire-answer-status';
-import { convertToLegacyFormat } from '../../util/convert-to-legacy-format';
+import { convertToLegacyFormat, getSiteUrlHashed } from '../../util/convert-to-legacy-format';
 
 
 
 
 const logger = loggerFactory('growi:routes:apiv3:questionnaire');
 const logger = loggerFactory('growi:routes:apiv3:questionnaire');
@@ -61,8 +62,8 @@ module.exports = (crowi: Crowi): Router => {
   };
   };
 
 
   router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-    const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
+    const growiInfo = await growiInfoService.getGrowiInfo(true);
+    const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
 
 
     try {
     try {
       const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
       const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
@@ -83,8 +84,9 @@ module.exports = (crowi: Crowi): Router => {
   router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const sendQuestionnaireAnswer = async() => {
     const sendQuestionnaireAnswer = async() => {
       const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
       const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-      const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
+      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
+      const growiInfo = await growiInfoService.getGrowiInfo(true);
+      const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
 
 
       const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
       const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
         satisfaction: req.body.satisfaction,
         satisfaction: req.body.satisfaction,
@@ -97,7 +99,7 @@ module.exports = (crowi: Crowi): Router => {
         answeredAt: new Date(),
         answeredAt: new Date(),
       };
       };
 
 
-      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer);
+      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer, isAppSiteUrlHashed);
 
 
       try {
       try {
         await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
         await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
@@ -131,8 +133,9 @@ module.exports = (crowi: Crowi): Router => {
   router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
     const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
       const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
       const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-      const userInfo = crowi.questionnaireService!.getUserInfo(user, growiInfo.appSiteUrlHashed);
+      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
+      const growiInfo = await growiInfoService.getGrowiInfo(true);
+      const userInfo = crowi.questionnaireService.getUserInfo(user, getSiteUrlHashed(growiInfo.appSiteUrl));
 
 
       const questionnaireAnswer: IQuestionnaireAnswer = {
       const questionnaireAnswer: IQuestionnaireAnswer = {
         growiInfo,
         growiInfo,
@@ -142,7 +145,7 @@ module.exports = (crowi: Crowi): Router => {
         questionnaireOrder: req.body.questionnaireOrderId,
         questionnaireOrder: req.body.questionnaireOrderId,
       };
       };
 
 
-      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer);
+      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer, isAppSiteUrlHashed);
 
 
       try {
       try {
         await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswerLegacy);
         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 axiosRetry from 'axios-retry';
 
 
+import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { getRandomIntInRange } from '~/utils/rand';
 import { getRandomIntInRange } from '~/utils/rand';
 
 
@@ -54,7 +55,8 @@ class QuestionnaireCronService {
   }
   }
 
 
   async executeJob(): Promise<void> {
   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 fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
       const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
       const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
@@ -84,14 +86,14 @@ class QuestionnaireCronService {
 
 
       axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
       axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
         // convert to legacy format
         // convert to legacy format
-        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer)),
+        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
       })
       })
         .then(async() => {
         .then(async() => {
           await QuestionnaireAnswer.deleteMany();
           await QuestionnaireAnswer.deleteMany();
         });
         });
       axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
       axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
         // convert to legacy format
         // convert to legacy format
-        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer)),
+        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
       })
       })
         .then(async() => {
         .then(async() => {
           await ProactiveQuestionnaireAnswer.deleteMany();
           await ProactiveQuestionnaireAnswer.deleteMany();

+ 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 { mock } from 'vitest-mock-extended';
 
 
 import pkg from '^/package.json';
 import pkg from '^/package.json';
 
 
 
 
 import type UserEvent from '~/server/events/user';
 import type UserEvent from '~/server/events/user';
-import { Config } from '~/server/models/config';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 
 
 import type Crowi from '../../../../server/crowi';
 import type Crowi from '../../../../server/crowi';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { UserType } from '../../interfaces/user-info';
 import { UserType } from '../../interfaces/user-info';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
@@ -26,21 +26,8 @@ describe('QuestionnaireService', () => {
   let user;
   let user;
 
 
   beforeAll(async() => {
   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.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>({
     const crowiMock = mock<Crowi>({
       version: appVersion,
       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;
     const userModelFactory = (await import('~/server/models/user')).default;
     User = userModelFactory(crowiMock);
     User = userModelFactory(crowiMock);
-    questionnaireService = new QuestionnaireService(crowiMock);
 
 
     await User.deleteMany({}); // clear users
     await User.deleteMany({}); // clear users
     user = await User.create({
     user = await User.create({
@@ -67,55 +53,8 @@ describe('QuestionnaireService', () => {
       password: 'usertestpass',
       password: 'usertestpass',
       createdAt: '2000-01-01',
       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', () => {
   describe('getUserInfo', () => {
@@ -318,8 +257,11 @@ describe('QuestionnaireService', () => {
     });
     });
 
 
     test('Should get questionnaire orders to show', async() => {
     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);
       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 crypto from 'crypto';
-import * as os from 'node:os';
-
 
 
 import type { IUserHasId } from '@growi/core';
 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 Crowi from '~/server/crowi';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 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 loggerFactory from '~/utils/logger';
 
 
 import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
 import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
@@ -36,64 +27,6 @@ class QuestionnaireService {
     this.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.
-    // 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 {
   getUserInfo(user: IUserHasId | null, appSiteUrlHashed: string): IUserInfo {
     if (user != null) {
     if (user != null) {
       const hasher = crypto.createHmac('sha256', appSiteUrlHashed);
       const hasher = crypto.createHmac('sha256', appSiteUrlHashed);

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

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

@@ -1,4 +1,5 @@
 import assert from 'assert';
 import assert from 'assert';
+import crypto from 'crypto';
 
 
 import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
 import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
 
 
@@ -12,18 +13,26 @@ function isLegacy<T extends { growiInfo: any }>(data: T): data is IHasGrowiAppIn
   return !('additionalInfo' in data.growiInfo);
   return !('additionalInfo' in data.growiInfo);
 }
 }
 
 
+export function getSiteUrlHashed(siteUrl: string): string {
+  const hasher = crypto.createHash('sha256');
+  hasher.update(siteUrl);
+  return hasher.digest('hex');
+}
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 // 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)) {
   if (isLegacy(questionnaireAnswer)) {
     return questionnaireAnswer;
     return questionnaireAnswer;
   }
   }
 
 
-  const { additionalInfo, ...rest } = questionnaireAnswer.growiInfo;
+  const { additionalInfo, appSiteUrl, ...rest } = questionnaireAnswer.growiInfo;
   assert(additionalInfo != null);
   assert(additionalInfo != null);
 
 
   return {
   return {
     ...questionnaireAnswer,
     ...questionnaireAnswer,
     growiInfo: {
     growiInfo: {
+      appSiteUrl: isAppSiteUrlHashed ? undefined : appSiteUrl,
+      appSiteUrlHashed: getSiteUrlHashed(appSiteUrl),
       ...rest,
       ...rest,
       ...additionalInfo,
       ...additionalInfo,
     },
     },

+ 24 - 0
apps/app/src/migrations/19700101000000-foremost-1010-20250109000000-generate-service-instance-id.js

@@ -0,0 +1,24 @@
+import mongoose from 'mongoose';
+import { v4 as uuidv4 } from 'uuid';
+
+import { configManager } from '~/server/service/config-manager';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:generate-service-instance-id');
+
+module.exports = {
+  async up(db) {
+    logger.info('Generate serviceInstanceId for the system');
+    await mongoose.connect(getMongoUri(), mongoOptions);
+
+    await configManager.loadConfigs();
+
+    await configManager.updateConfig('app:serviceInstanceId', uuidv4(), { skipPubsub: true });
+  },
+
+  async down() {
+    // No rollback
+  },
+};

+ 2 - 2
apps/app/src/pages/admin/security.page.tsx

@@ -92,9 +92,9 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
 const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
 const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
-  const { appService, mailService } = crowi;
+  const { growiInfoService, mailService } = crowi;
 
 
-  props.siteUrl = appService.getSiteUrl();
+  props.siteUrl = growiInfoService.getSiteUrl();
   props.isMailerSetup = mailService.isMailerSetup;
   props.isMailerSetup = mailService.isMailerSetup;
 };
 };
 
 

+ 2 - 2
apps/app/src/pages/admin/slack-integration.page.tsx

@@ -49,9 +49,9 @@ const AdminSlackIntegrationPage: NextPage<Props> = (props) => {
 const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
 const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
-  const { appService } = crowi;
+  const { growiInfoService } = crowi;
 
 
-  props.siteUrl = appService.getSiteUrl();
+  props.siteUrl = growiInfoService.getSiteUrl();
 };
 };
 
 
 
 

+ 2 - 1
apps/app/src/pages/utils/commons.ts

@@ -17,6 +17,7 @@ import { detectLocaleFromBrowserAcceptLanguage } from '~/server/util/locale-util
 import {
 import {
   useCurrentProductNavWidth, useCurrentSidebarContents, usePreferCollapsedMode,
   useCurrentProductNavWidth, useCurrentSidebarContents, usePreferCollapsedMode,
 } from '~/stores/ui';
 } from '~/stores/ui';
+import { getGrowiVersion } from '~/utils/growi-version';
 
 
 export type CommonProps = {
 export type CommonProps = {
   namespacesRequired: string[], // i18next
   namespacesRequired: string[], // i18next
@@ -92,7 +93,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     customTitleTemplate: customizeService.customTitleTemplate,
     customTitleTemplate: customizeService.customTitleTemplate,
     csrfToken: req.csrfToken(),
     csrfToken: req.csrfToken(),
     isContainerFluid: configManager.getConfig('customize:isContainerFluid') ?? false,
     isContainerFluid: configManager.getConfig('customize:isContainerFluid') ?? false,
-    growiVersion: crowi.version,
+    growiVersion: getGrowiVersion(),
     isMaintenanceMode,
     isMaintenanceMode,
     redirectDestination,
     redirectDestination,
     currentUser,
     currentUser,

+ 4 - 4
apps/app/src/server/app.ts

@@ -1,8 +1,6 @@
 import type Logger from 'bunyan';
 import type Logger from 'bunyan';
 
 
-import pkg from '^/package.json';
-
-import { startInstrumentation } from '~/features/opentelemetry/server';
+import { initServiceInstanceId, startInstrumentation } from '~/features/opentelemetry/server';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { hasProcessFlag } from '~/utils/process-utils';
 import { hasProcessFlag } from '~/utils/process-utils';
 
 
@@ -23,12 +21,14 @@ process.on('unhandledRejection', (reason, p) => {
 async function main() {
 async function main() {
   try {
   try {
     // start OpenTelemetry
     // start OpenTelemetry
-    await startInstrumentation(pkg.version);
+    await startInstrumentation();
 
 
     const Crowi = (await import('./crowi')).default;
     const Crowi = (await import('./crowi')).default;
     const growi = new Crowi();
     const growi = new Crowi();
     const server = await growi.start();
     const server = await growi.start();
 
 
+    await initServiceInstanceId();
+
     if (hasProcessFlag('ci')) {
     if (hasProcessFlag('ci')) {
       logger.info('"--ci" flag is detected. Exit process.');
       logger.info('"--ci" flag is detected. Exit process.');
       server.close(() => {
       server.close(() => {

+ 2 - 1
apps/app/src/server/crowi/express-init.js

@@ -10,6 +10,7 @@ import registerSafeRedirectFactory from '../middlewares/safe-redirect';
 
 
 const logger = loggerFactory('growi:crowi:express-init');
 const logger = loggerFactory('growi:crowi:express-init');
 
 
+/** @param {import('./index').default} crowi Crowi instance */
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   const express = require('express');
   const express = require('express');
   const compression = require('compression');
   const compression = require('compression');
@@ -72,7 +73,7 @@ module.exports = function(crowi, app) {
     app.set('tzoffset', crowi.appService.getTzoffset());
     app.set('tzoffset', crowi.appService.getTzoffset());
 
 
     res.locals.req = req;
     res.locals.req = req;
-    res.locals.baseUrl = crowi.appService.getSiteUrl();
+    res.locals.baseUrl = crowi.growiInfoService.getSiteUrl();
     res.locals.env = env;
     res.locals.env = env;
     res.locals.now = now;
     res.locals.now = now;
 
 

+ 11 - 3
apps/app/src/server/crowi/index.js

@@ -8,13 +8,12 @@ import lsxRoutes from '@growi/remark-lsx/dist/server/index.cjs';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 import next from 'next';
 import next from 'next';
 
 
-import pkg from '^/package.json';
-
 import { KeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
 import { KeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
 import { startCronIfEnabled as startOpenaiCronIfEnabled } from '~/features/openai/server/services/cron';
 import { startCronIfEnabled as startOpenaiCronIfEnabled } from '~/features/openai/server/services/cron';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
 import QuestionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
+import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 import { projectRoot } from '~/utils/project-dir-utils';
 
 
@@ -70,6 +69,9 @@ class Crowi {
   /** @type {FileUploader} */
   /** @type {FileUploader} */
   fileUploadService;
   fileUploadService;
 
 
+  /** @type {import('../service/growi-info').GrowiInfoService} */
+  growiInfoService;
+
   /** @type {import('../service/page').IPageService} */
   /** @type {import('../service/page').IPageService} */
   pageService;
   pageService;
 
 
@@ -95,7 +97,7 @@ class Crowi {
   userNotificationService;
   userNotificationService;
 
 
   constructor() {
   constructor() {
-    this.version = pkg.version;
+    this.version = getGrowiVersion();
 
 
     this.publicDir = path.join(projectRoot, 'public') + sep;
     this.publicDir = path.join(projectRoot, 'public') + sep;
     this.resourceDir = path.join(projectRoot, 'resource') + sep;
     this.resourceDir = path.join(projectRoot, 'resource') + sep;
@@ -177,6 +179,7 @@ Crowi.prototype.init = async function() {
   ]);
   ]);
 
 
   await Promise.all([
   await Promise.all([
+    this.setupGrowiInfoService(),
     this.setupPassport(),
     this.setupPassport(),
     this.setupSearcher(),
     this.setupSearcher(),
     this.setupMailer(),
     this.setupMailer(),
@@ -659,6 +662,11 @@ Crowi.prototype.setUpFileUploaderSwitchService = async function() {
   }
   }
 };
 };
 
 
+Crowi.prototype.setupGrowiInfoService = async function() {
+  const { growiInfoService } = await import('../service/growi-info');
+  this.growiInfoService = growiInfoService;
+};
+
 /**
 /**
  * setup AttachmentService
  * setup AttachmentService
  */
  */

+ 1 - 1
apps/app/src/server/models/obsolete-page.js

@@ -84,7 +84,7 @@ export const populateDataToShowRevision = (page, userPublicFields, shouldExclude
 };
 };
 /* eslint-enable object-curly-newline, object-property-newline */
 /* eslint-enable object-curly-newline, object-property-newline */
 
 
-
+/** @param {import('~/server/crowi').default | null} crowi Crowi instance */
 export const getPageSchema = (crowi) => {
 export const getPageSchema = (crowi) => {
   let pageEvent;
   let pageEvent;
 
 

+ 5 - 2
apps/app/src/server/models/slack-app-integration.js

@@ -38,8 +38,10 @@ class SlackAppIntegration {
     let generateTokens;
     let generateTokens;
 
 
     // get salt strings
     // get salt strings
-    const saltForGtoP = this.crowi.configManager.getConfig('slackbot:withProxy:saltForGtoP');
-    const saltForPtoG = this.crowi.configManager.getConfig('slackbot:withProxy:saltForPtoG');
+    /** @type {import('~/server/crowi').default} Crowi instance */
+    const crowi = this.crowi;
+    const saltForGtoP = crowi.configManager.getConfig('slackbot:withProxy:saltForGtoP');
+    const saltForPtoG = crowi.configManager.getConfig('slackbot:withProxy:saltForPtoG');
 
 
     do {
     do {
       generateTokens = this.generateAccessTokens(saltForGtoP, saltForPtoG);
       generateTokens = this.generateAccessTokens(saltForGtoP, saltForPtoG);
@@ -55,6 +57,7 @@ class SlackAppIntegration {
 
 
 }
 }
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 const factory = (crowi) => {
 const factory = (crowi) => {
   const modelExists = getModelSafely('SlackAppIntegration');
   const modelExists = getModelSafely('SlackAppIntegration');
   if (modelExists != null) {
   if (modelExists != null) {

+ 2 - 1
apps/app/src/server/routes/apiv3/admin-home.ts

@@ -1,4 +1,5 @@
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { getGrowiVersion } from '~/utils/growi-version';
 
 
 const express = require('express');
 const express = require('express');
 
 
@@ -87,7 +88,7 @@ module.exports = (crowi) => {
     const runtimeVersions = await getRuntimeVersions();
     const runtimeVersions = await getRuntimeVersions();
 
 
     const adminHomeParams = {
     const adminHomeParams = {
-      growiVersion: crowi.version,
+      growiVersion: getGrowiVersion(),
       nodeVersion: runtimeVersions.node ?? '-',
       nodeVersion: runtimeVersions.node ?? '-',
       npmVersion: runtimeVersions.npm ?? '-',
       npmVersion: runtimeVersions.npm ?? '-',
       pnpmVersion: runtimeVersions.pnpm ?? '-',
       pnpmVersion: runtimeVersions.pnpm ?? '-',

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

@@ -132,6 +132,7 @@ const {
  *            description: temporary URL cached
  *            description: temporary URL cached
  *            example: "https://example.com/attachment/5e0734e072560e001761fa67"
  *            example: "https://example.com/attachment/5e0734e072560e001761fa67"
  */
  */
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);

+ 2 - 1
apps/app/src/server/routes/apiv3/forgot-password.js

@@ -7,6 +7,7 @@ import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity
 import injectResetOrderByTokenMiddleware from '~/server/middlewares/inject-reset-order-by-token-middleware';
 import injectResetOrderByTokenMiddleware from '~/server/middlewares/inject-reset-order-by-token-middleware';
 import PasswordResetOrder from '~/server/models/password-reset-order';
 import PasswordResetOrder from '~/server/models/password-reset-order';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { growiInfoService } from '~/server/service/growi-info';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
@@ -119,7 +120,7 @@ module.exports = (crowi) => {
   router.post('/', checkPassportStrategyMiddleware, validator.email, apiV3FormValidator, addActivity, async(req, res) => {
   router.post('/', checkPassportStrategyMiddleware, validator.email, apiV3FormValidator, addActivity, async(req, res) => {
     const { email } = req.body;
     const { email } = req.body;
     const locale = configManager.getConfig('app:globalLang');
     const locale = configManager.getConfig('app:globalLang');
-    const appUrl = appService.getSiteUrl();
+    const appUrl = growiInfoService.getSiteUrl();
 
 
     try {
     try {
       const user = await User.findOne({ email });
       const user = await User.findOne({ email });

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

@@ -54,7 +54,7 @@ const router = express.Router();
  *            type: boolean
  *            type: boolean
  *            description: whether the current importing job exists or not
  *            description: whether the current importing job exists or not
  */
  */
-
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 export default function route(crowi) {
 export default function route(crowi) {
   const { growiBridgeService, socketIoService } = crowi;
   const { growiBridgeService, socketIoService } = crowi;
   const importService = getImportService(crowi);
   const importService = getImportService(crowi);

+ 1 - 0
apps/app/src/server/routes/apiv3/index.js

@@ -21,6 +21,7 @@ const router = express.Router();
 const routerForAdmin = express.Router();
 const routerForAdmin = express.Router();
 const routerForAuth = express.Router();
 const routerForAuth = express.Router();
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi, app) => {
 module.exports = (crowi, app) => {
   const isInstalled = crowi.configManager.getConfig('app:installed');
   const isInstalled = crowi.configManager.getConfig('app:installed');
   const minPasswordLength = crowi.configManager.getConfig('app:minPasswordLength');
   const minPasswordLength = crowi.configManager.getConfig('app:minPasswordLength');

+ 1 - 0
apps/app/src/server/routes/apiv3/markdown-setting.js

@@ -82,6 +82,7 @@ const validator = {
  *              description: attr whitelist
  *              description: attr whitelist
  */
  */
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);

+ 1 - 0
apps/app/src/server/routes/apiv3/notification-setting.js

@@ -86,6 +86,7 @@ const validator = {
  *              type: string
  *              type: string
  *              description: trigger events for notify
  *              description: trigger events for notify
  */
  */
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);

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

@@ -57,7 +57,7 @@ const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
  *            description: Count of tagged pages
  *            description: Count of tagged pages
  *            example: 3
  *            example: 3
  */
  */
-
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);

+ 1 - 0
apps/app/src/server/routes/apiv3/personal-setting.js

@@ -66,6 +66,7 @@ const router = express.Router();
  *          accountId:
  *          accountId:
  *            type: string
  *            type: string
  */
  */
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);

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

@@ -18,6 +18,7 @@ const router = express.Router();
 
 
 const MIGRATION_FILE_NAME = '20211227060705-revision-path-to-page-id-schema-migration--fixed-7549';
 const MIGRATION_FILE_NAME = '20211227060705-revision-path-to-page-id-schema-migration--fixed-7549';
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const certifySharedPage = require('../../middlewares/certify-shared-page')(crowi);
   const certifySharedPage = require('../../middlewares/certify-shared-page')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);

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

@@ -20,6 +20,7 @@ const validator = {};
 
 
 const today = new Date();
 const today = new Date();
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequired = require('../../middlewares/login-required')(crowi);
   const loginRequired = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);

+ 1 - 0
apps/app/src/server/routes/apiv3/slack-integration-legacy-settings.js

@@ -41,6 +41,7 @@ const validator = {
  *            type: string
  *            type: string
  *            description: OAuth access token
  *            description: OAuth access token
  */
  */
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);

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

@@ -45,7 +45,7 @@ const router = express.Router();
  *            type: string
  *            type: string
  */
  */
 
 
-
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);

+ 5 - 4
apps/app/src/server/routes/apiv3/slack-integration.js

@@ -10,6 +10,7 @@ import createError from 'http-errors';
 
 
 import { SlackCommandHandlerError } from '~/server/models/vo/slack-command-handler-error';
 import { SlackCommandHandlerError } from '~/server/models/vo/slack-command-handler-error';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { growiInfoService } from '~/server/service/growi-info';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
@@ -104,7 +105,7 @@ module.exports = (crowi) => {
       id: req.body.channel_id,
       id: req.body.channel_id,
       name: req.body.channel_name,
       name: req.body.channel_name,
     };
     };
-    const siteUrl = crowi.appService.getSiteUrl();
+    const siteUrl = growiInfoService.getSiteUrl();
 
 
     let commandPermission;
     let commandPermission;
     if (extractPermissions != null) { // with proxy
     if (extractPermissions != null) { // with proxy
@@ -145,7 +146,7 @@ module.exports = (crowi) => {
     res.send();
     res.send();
 
 
     const { interactionPayloadAccessor } = req;
     const { interactionPayloadAccessor } = req;
-    const siteUrl = crowi.appService.getSiteUrl();
+    const siteUrl = growiInfoService.getSiteUrl();
 
 
     const { actionId, callbackId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
     const { actionId, callbackId } = interactionPayloadAccessor.getActionIdAndCallbackIdFromPayLoad();
     const callbacIdkOrActionId = callbackId || actionId;
     const callbacIdkOrActionId = callbackId || actionId;
@@ -214,7 +215,7 @@ module.exports = (crowi) => {
   function getRespondUtil(responseUrl) {
   function getRespondUtil(responseUrl) {
     const proxyUri = slackIntegrationService.proxyUriForCurrentType ?? null; // can be null
     const proxyUri = slackIntegrationService.proxyUriForCurrentType ?? null; // can be null
 
 
-    const appSiteUrl = crowi.appService.getSiteUrl();
+    const appSiteUrl = growiInfoService.getSiteUrl();
     if (appSiteUrl == null || appSiteUrl === '') {
     if (appSiteUrl == null || appSiteUrl === '') {
       logger.error('App site url must exist.');
       logger.error('App site url must exist.');
       throw SlackCommandHandlerError('App site url must exist.');
       throw SlackCommandHandlerError('App site url must exist.');
@@ -268,7 +269,7 @@ module.exports = (crowi) => {
 
 
     // Send response immediately to avoid opelation_timeout error
     // Send response immediately to avoid opelation_timeout error
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    const appSiteUrl = crowi.appService.getSiteUrl();
+    const appSiteUrl = growiInfoService.getSiteUrl();
     try {
     try {
       await respondUtil.respond({
       await respondUtil.respond({
         text: 'Processing your request ...',
         text: 'Processing your request ...',

+ 1 - 0
apps/app/src/server/routes/apiv3/staffs.js

@@ -21,6 +21,7 @@ const compareFunction = function(a, b) {
   return a.order - b.order;
   return a.order - b.order;
 };
 };
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
 
 
   router.get('/', async(req, res) => {
   router.get('/', async(req, res) => {

+ 3 - 2
apps/app/src/server/routes/apiv3/user-activation.ts

@@ -8,6 +8,7 @@ import { SupportedAction } from '~/interfaces/activity';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import UserRegistrationOrder from '~/server/models/user-registration-order';
 import UserRegistrationOrder from '~/server/models/user-registration-order';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { growiInfoService } from '~/server/service/growi-info';
 import { getTranslation } from '~/server/service/i18next';
 import { getTranslation } from '~/server/service/i18next';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -146,7 +147,7 @@ export const completeRegistrationAction = (crowi) => {
             const appTitle = appService.getAppTitle();
             const appTitle = appService.getAppTitle();
             const locale = configManager.getConfig('app:globalLang');
             const locale = configManager.getConfig('app:globalLang');
             const template = path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`);
             const template = path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`);
-            const url = appService.getSiteUrl();
+            const url = growiInfoService.getSiteUrl();
 
 
             sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
             sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url);
           }
           }
@@ -219,7 +220,7 @@ async function makeRegistrationEmailToken(email, crowi) {
   }
   }
 
 
   const locale = configManager.getConfig('app:globalLang');
   const locale = configManager.getConfig('app:globalLang');
-  const appUrl = appService.getSiteUrl();
+  const appUrl = growiInfoService.getSiteUrl();
 
 
   const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
   const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
   const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
   const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;

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

@@ -16,6 +16,7 @@ import ExternalAccount from '~/server/models/external-account';
 import { serializePageSecurely } from '~/server/models/serializers';
 import { serializePageSecurely } from '~/server/models/serializers';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { growiInfoService } from '~/server/service/growi-info';
 import { deleteCompletelyUserHomeBySystem } from '~/server/service/page/delete-completely-user-home-by-system';
 import { deleteCompletelyUserHomeBySystem } from '~/server/service/page/delete-completely-user-home-by-system';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -74,6 +75,7 @@ const validator = {};
  *            example: 2010-01-01T00:00:00.000Z
  *            example: 2010-01-01T00:00:00.000Z
  */
  */
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
@@ -159,7 +161,7 @@ module.exports = (crowi) => {
           vars: {
           vars: {
             email: user.email,
             email: user.email,
             password: user.password,
             password: user.password,
-            url: crowi.appService.getSiteUrl(),
+            url: growiInfoService.getSiteUrl(),
             appTitle,
             appTitle,
           },
           },
         });
         });
@@ -190,7 +192,7 @@ module.exports = (crowi) => {
       vars: {
       vars: {
         email: user.email,
         email: user.email,
         password: user.password,
         password: user.password,
-        url: crowi.appService.getSiteUrl(),
+        url: growiInfoService.getSiteUrl(),
         appTitle,
         appTitle,
       },
       },
     });
     });

+ 4 - 1
apps/app/src/server/routes/login.js

@@ -2,9 +2,12 @@ import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { growiInfoService } from '../service/growi-info';
+
 // disable all of linting
 // disable all of linting
 // because this file is a deprecated legacy of Crowi
 // because this file is a deprecated legacy of Crowi
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:login');
   const logger = loggerFactory('growi:routes:login');
   const path = require('path');
   const path = require('path');
@@ -30,7 +33,7 @@ module.exports = function(crowi, app) {
         vars: {
         vars: {
           adminUser: admin,
           adminUser: admin,
           createdUser: userData,
           createdUser: userData,
-          url: appService.getSiteUrl(),
+          url: growiInfoService.getSiteUrl(),
           appTitle,
           appTitle,
         },
         },
       });
       });

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

@@ -1,5 +1,4 @@
 import { ConfigSource } from '@growi/core/dist/interfaces';
 import { ConfigSource } from '@growi/core/dist/interfaces';
-import { pathUtils } from '@growi/core/dist/utils';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -76,27 +75,6 @@ export default class AppService implements S2sMessageHandlable {
     return configManager.getConfig('app:title') ?? 'GROWI';
     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() {
   getTzoffset() {
     return -(configManager.getConfig('app:timezone') || 9) * 60;
     return -(configManager.getConfig('app:timezone') || 9) * 60;
   }
   }

+ 79 - 0
apps/app/src/server/service/config-manager/config-definition.ts

@@ -31,6 +31,7 @@ export const CONFIG_KEYS = [
 
 
   // App Settings
   // App Settings
   'app:installed',
   'app:installed',
+  'app:serviceInstanceId',
   'app:isV5Compatible',
   'app:isV5Compatible',
   'app:isMaintenanceMode',
   'app:isMaintenanceMode',
   'app:confidential',
   'app:confidential',
@@ -138,8 +139,18 @@ export const CONFIG_KEYS = [
   'security:passport-github:clientSecret',
   'security:passport-github:clientSecret',
   'security:passport-github:isSameUsernameTreatedAsIdenticalUser',
   'security:passport-github:isSameUsernameTreatedAsIdenticalUser',
   'security:passport-github:isSameEmailTreatedAsIdenticalUser',
   'security:passport-github:isSameEmailTreatedAsIdenticalUser',
+  'security:passport-oidc:clientId',
+  'security:passport-oidc:clientSecret',
   'security:passport-oidc:isEnabled',
   'security:passport-oidc:isEnabled',
   'security:passport-oidc:issuerHost',
   'security:passport-oidc:issuerHost',
+  'security:passport-oidc:authorizationEndpoint',
+  'security:passport-oidc:tokenEndpoint',
+  'security:passport-oidc:revocationEndpoint',
+  'security:passport-oidc:introspectionEndpoint',
+  'security:passport-oidc:userInfoEndpoint',
+  'security:passport-oidc:endSessionEndpoint',
+  'security:passport-oidc:registrationEndpoint',
+  'security:passport-oidc:jwksUri',
   'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser',
   'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser',
   'security:passport-oidc:isSameEmailTreatedAsIdenticalUser',
   'security:passport-oidc:isSameEmailTreatedAsIdenticalUser',
 
 
@@ -176,6 +187,16 @@ export const CONFIG_KEYS = [
   // GridFS Settings
   // GridFS Settings
   'gridfs:totalLimit',
   'gridfs:totalLimit',
 
 
+  // Mail Settings
+  'mail:from',
+  'mail:transmissionMethod',
+  'mail:smtpHost',
+  'mail:smtpPort',
+  'mail:smtpUser',
+  'mail:smtpPassword',
+  'mail:sesSecretAccessKey',
+  'mail:sesAccessKeyId',
+
   // Customize Settings
   // Customize Settings
   'customize:isEmailPublishedForNewUser',
   'customize:isEmailPublishedForNewUser',
   'customize:css',
   'customize:css',
@@ -338,6 +359,9 @@ export const CONFIG_DEFINITIONS = {
   'app:installed': defineConfig<boolean>({
   'app:installed': defineConfig<boolean>({
     defaultValue: false,
     defaultValue: false,
   }),
   }),
+  'app:serviceInstanceId': defineConfig<string>({
+    defaultValue: '',
+  }),
   'app:isV5Compatible': defineConfig<boolean>({
   'app:isV5Compatible': defineConfig<boolean>({
     defaultValue: false,
     defaultValue: false,
   }),
   }),
@@ -717,12 +741,42 @@ export const CONFIG_DEFINITIONS = {
   'security:passport-github:isSameEmailTreatedAsIdenticalUser': defineConfig<boolean>({
   'security:passport-github:isSameEmailTreatedAsIdenticalUser': defineConfig<boolean>({
     defaultValue: false,
     defaultValue: false,
   }),
   }),
+  'security:passport-oidc:clientId': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:clientSecret': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
   'security:passport-oidc:isEnabled': defineConfig<boolean>({
   'security:passport-oidc:isEnabled': defineConfig<boolean>({
     defaultValue: false,
     defaultValue: false,
   }),
   }),
   'security:passport-oidc:issuerHost': defineConfig<string | undefined>({
   'security:passport-oidc:issuerHost': defineConfig<string | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
+  'security:passport-oidc:authorizationEndpoint': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:tokenEndpoint': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:revocationEndpoint': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:introspectionEndpoint': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:userInfoEndpoint': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:endSessionEndpoint': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:registrationEndpoint': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'security:passport-oidc:jwksUri': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
   'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser': defineConfig<boolean>({
   'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser': defineConfig<boolean>({
     defaultValue: false,
     defaultValue: false,
   }),
   }),
@@ -828,6 +882,31 @@ export const CONFIG_DEFINITIONS = {
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
 
 
+  // Mail Settings
+  'mail:from': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:transmissionMethod': defineConfig<'smtp' | 'ses' | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:smtpHost': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:smtpPort': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:smtpUser': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:smtpPassword': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:sesAccessKeyId': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
+  'mail:sesSecretAccessKey': defineConfig<string | undefined>({
+    defaultValue: undefined,
+  }),
 
 
   // Customize Settings
   // Customize Settings
   'customize:isEmailPublishedForNewUser': defineConfig<boolean>({
   'customize:isEmailPublishedForNewUser': defineConfig<boolean>({

+ 9 - 0
apps/app/src/server/service/config-manager/config-manager.ts

@@ -101,6 +101,15 @@ export class ConfigManager implements IConfigManagerForApp, S2sMessageHandlable
     return value;
     return value;
   }
   }
 
 
+  /**
+   * @deprecated
+   */
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  getConfigLegacy<T = any>(key: string, source?: ConfigSource): T {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    return this.getConfig(key as any, source) as T;
+  }
+
   private checkDifference<K extends ConfigKey>(key: K, value: ConfigValues[K], source?: ConfigSource): void {
   private checkDifference<K extends ConfigKey>(key: K, value: ConfigValues[K], source?: ConfigSource): void {
     const valueByLegacy = (() => {
     const valueByLegacy = (() => {
       if (source === ConfigSource.env) {
       if (source === ConfigSource.env) {

+ 4 - 2
apps/app/src/server/service/export.js

@@ -1,7 +1,9 @@
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
+import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { configManager } from './config-manager';
 import { configManager } from './config-manager';
+import { growiInfoService } from './growi-info';
 
 
 const logger = loggerFactory('growi:services:ExportService'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:services:ExportService'); // eslint-disable-line no-unused-vars
 
 
@@ -97,8 +99,8 @@ class ExportService {
     const passwordSeed = this.crowi.env.PASSWORD_SEED || null;
     const passwordSeed = this.crowi.env.PASSWORD_SEED || null;
 
 
     const metaData = {
     const metaData = {
-      version: this.crowi.version,
-      url: this.appService.getSiteUrl(),
+      version: getGrowiVersion(),
+      url: growiInfoService.getSiteUrl(),
       passwordSeed,
       passwordSeed,
       exportedAt: new Date(),
       exportedAt: new Date(),
       envVars: configManager.getManagedEnvVars(),
       envVars: configManager.getManagedEnvVars(),

+ 4 - 2
apps/app/src/server/service/g2g-transfer.ts

@@ -17,6 +17,7 @@ import TransferKeyModel from '~/server/models/transfer-key';
 import { getImportService, type ImportSettings } from '~/server/service/import';
 import { getImportService, type ImportSettings } from '~/server/service/import';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { createBatchStream } from '~/server/util/batch-stream';
 import axios from '~/utils/axios';
 import axios from '~/utils/axios';
+import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
 import { TransferKey } from '~/utils/vo/transfer-key';
 
 
@@ -256,7 +257,7 @@ export class G2GTransferPusherService implements Pusher {
   public async getTransferability(destGROWIInfo: IDataGROWIInfo): Promise<Transferability> {
   public async getTransferability(destGROWIInfo: IDataGROWIInfo): Promise<Transferability> {
     const { fileUploadService } = this.crowi;
     const { fileUploadService } = this.crowi;
 
 
-    const version = this.crowi.version;
+    const version = getGrowiVersion();
     if (version !== destGROWIInfo.version) {
     if (version !== destGROWIInfo.version) {
       return {
       return {
         canTransfer: false,
         canTransfer: false,
@@ -550,7 +551,8 @@ export class G2GTransferReceiverService implements Receiver {
   }
   }
 
 
   public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
   public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
-    const { version, fileUploadService } = this.crowi;
+    const { fileUploadService } = this.crowi;
+    const version = getGrowiVersion();
     const userUpperLimit = configManager.getConfig('security:userUpperLimit');
     const userUpperLimit = configManager.getConfig('security:userUpperLimit');
     const fileUploadDisabled = configManager.getConfig('app:fileUploadDisabled');
     const fileUploadDisabled = configManager.getConfig('app:fileUploadDisabled');
     const fileUploadTotalLimit = fileUploadService.getFileUploadTotalLimit();
     const fileUploadTotalLimit = fileUploadService.getFileUploadTotalLimit();

+ 2 - 1
apps/app/src/server/service/global-notification/global-notification-slack.js

@@ -6,6 +6,7 @@ import loggerFactory from '~/utils/logger';
 import {
 import {
   prepareSlackMessageForGlobalNotification,
   prepareSlackMessageForGlobalNotification,
 } from '../../util/slack';
 } from '../../util/slack';
+import { growiInfoService } from '../growi-info';
 
 
 const logger = loggerFactory('growi:service:GlobalNotificationSlackService'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:service:GlobalNotificationSlackService'); // eslint-disable-line no-unused-vars
 const urljoin = require('url-join');
 const urljoin = require('url-join');
@@ -65,7 +66,7 @@ class GlobalNotificationSlackService {
    * @return  {string} slack message body
    * @return  {string} slack message body
    */
    */
   generateMessageBody(event, id, path, triggeredBy, { comment, oldPath }) {
   generateMessageBody(event, id, path, triggeredBy, { comment, oldPath }) {
-    const siteUrl = this.crowi.appService.getSiteUrl();
+    const siteUrl = growiInfoService.getSiteUrl();
     const parmaLink = `<${urljoin(siteUrl, id)}|${path}>`;
     const parmaLink = `<${urljoin(siteUrl, id)}|${path}>`;
     const pathLink = `<${urljoin(siteUrl, encodeSpaces(path))}|${path}>`;
     const pathLink = `<${urljoin(siteUrl, encodeSpaces(path))}|${path}>`;
     const username = `<${urljoin(siteUrl, 'user', triggeredBy.username)}|${triggeredBy.username}>`;
     const username = `<${urljoin(siteUrl, 'user', triggeredBy.username)}|${triggeredBy.username}>`;

+ 7 - 3
apps/app/src/server/service/global-notification/index.js

@@ -1,14 +1,18 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:service:GlobalNotificationService');
 const logger = loggerFactory('growi:service:GlobalNotificationService');
-const GloabalNotificationSlack = require('./global-notification-slack');
 const GloabalNotificationMail = require('./global-notification-mail');
 const GloabalNotificationMail = require('./global-notification-mail');
+const GloabalNotificationSlack = require('./global-notification-slack');
 
 
 /**
 /**
  * service class of GlobalNotificationSetting
  * service class of GlobalNotificationSetting
  */
  */
 class GlobalNotificationService {
 class GlobalNotificationService {
 
 
+  /** @type {import('~/server/crowi').default} Crowi instance */
+  crowi;
+
+  /** @param {import('~/server/crowi').default} crowi Crowi instance */
   constructor(crowi) {
   constructor(crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
     this.defaultLang = 'en_US'; // TODO: get defaultLang from app global config
     this.defaultLang = 'en_US'; // TODO: get defaultLang from app global config
@@ -67,9 +71,9 @@ class GlobalNotificationService {
       case this.Page.GRANT_SPECIFIED:
       case this.Page.GRANT_SPECIFIED:
         return false;
         return false;
       case this.Page.GRANT_OWNER:
       case this.Page.GRANT_OWNER:
-        return (this.crowi.configManager.getConfig('notification:owner-page:isEnabled') || false);
+        return (this.crowi.configManager.getConfig('notification:owner-page:isEnabled'));
       case this.Page.GRANT_USER_GROUP:
       case this.Page.GRANT_USER_GROUP:
-        return (this.crowi.configManager.getConfig('notification:group-page:isEnabled') || false);
+        return (this.crowi.configManager.getConfig('notification:group-page:isEnabled'));
     }
     }
   }
   }
 
 

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

@@ -0,0 +1,117 @@
+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 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(),
+          });
+        }
+      }),
+    });
+
+    const userModelFactory = (await import('~/server/models/user')).default;
+    User = userModelFactory(crowiMock);
+
+    await User.deleteMany({}); // clear users
+  });
+
+  describe('getGrowiInfo', () => {
+
+    test('Should get correct GROWI info', async() => {
+      const growiInfo = await growiInfoService.getGrowiInfo();
+
+      assert(growiInfo != null);
+
+      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).osInfo;
+
+      expect(growiInfo).toEqual({
+        version: appVersion,
+        appSiteUrl: 'http://growi.test.jp',
+        serviceInstanceId: '',
+        type: 'on-premise',
+        wikiType: 'closed',
+        deploymentType: 'growi-docker-compose',
+      });
+    });
+
+    test('Should get correct GROWI info with additionalInfo', async() => {
+      // arrange
+      await User.create({
+        username: 'growiinfo test user',
+        createdAt: '2000-01-01',
+      });
+
+      // act
+      const growiInfo = await growiInfoService.getGrowiInfo(true);
+
+      // assert
+      assert(growiInfo != null);
+
+      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).osInfo;
+
+      expect(growiInfo).toEqual({
+        version: appVersion,
+        appSiteUrl: 'http://growi.test.jp',
+        serviceInstanceId: '',
+        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'],
+        },
+      });
+    });
+
+  });
+});

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

@@ -0,0 +1,111 @@
+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 { Config } from '~/server/models/config';
+import { aclService } from '~/server/service/acl';
+import { configManager } from '~/server/service/config-manager';
+import { getGrowiVersion } from '~/utils/growi-version';
+
+import type { IGrowiAppAdditionalInfo } from '../../../features/questionnaire/interfaces/growi-app-info';
+
+
+export class GrowiInfoService {
+
+  /**
+   * 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.
+   */
+  getSiteUrl(): string {
+    const siteUrl = configManager.getConfig('app:siteUrl');
+    if (siteUrl != null) {
+      return pathUtils.removeTrailingSlash(siteUrl);
+    }
+    return siteUrl ?? '[The site URL is not set. Please set it!]';
+  }
+
+  /**
+   * Get GROWI information
+   */
+  getGrowiInfo(): Promise<IGrowiInfo<Record<string, never>>>;
+
+  /**
+   * Get GROWI information with additional information
+   * @param includeAdditionalInfo whether to include additional information
+   */
+  getGrowiInfo(includeAdditionalInfo: true): Promise<IGrowiInfo<IGrowiAppAdditionalInfo>>;
+
+  async getGrowiInfo(includeAdditionalInfo?: boolean): Promise<IGrowiInfo<Record<string, never>> | IGrowiInfo<IGrowiAppAdditionalInfo>> {
+
+    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(),
+        platform: os.platform(),
+        arch: os.arch(),
+        totalmem: os.totalmem(),
+      },
+      appSiteUrl,
+      type: configManager.getConfig('app:serviceType'),
+      wikiType,
+      deploymentType: configManager.getConfig('app:deploymentType'),
+    } satisfies IGrowiInfo<Record<string, never>>;
+
+    if (!includeAdditionalInfo) {
+      return baseInfo;
+    }
+
+    return {
+      ...baseInfo,
+      additionalInfo: await this.getAdditionalInfo(),
+    };
+  }
+
+  private async getAdditionalInfo(): Promise<IGrowiAppAdditionalInfo> {
+    const User = mongoose.model<IUser, Model<IUser>>('User');
+
+    // 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 });
+    const installedAt = installedAtByOldestUser ?? appInstalledConfig?.createdAt ?? oldestConfig!.createdAt ?? null;
+
+    const currentUsersCount = await User.countDocuments();
+    const currentActiveUsersCount = await (User as any).countActiveUsers();
+
+    const activeExternalAccountTypes: IExternalAuthProviderType[] = Object.values(IExternalAuthProviderType).filter((type) => {
+      return configManager.getConfig(`security:passport-${type}:isEnabled`);
+    });
+
+    return {
+      installedAt,
+      installedAtByOldestUser,
+      currentUsersCount,
+      currentActiveUsersCount,
+      attachmentType: configManager.getConfig('app:fileUploadType'),
+      activeExternalAccountTypes,
+    };
+  }
+
+}
+
+export const growiInfoService = new GrowiInfoService();

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

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

+ 2 - 1
apps/app/src/server/service/import/import.ts

@@ -17,6 +17,7 @@ import { ImportMode } from '~/models/admin/import-mode';
 import type Crowi from '~/server/crowi';
 import type Crowi from '~/server/crowi';
 import { setupIndependentModels } from '~/server/crowi/setup-models';
 import { setupIndependentModels } from '~/server/crowi/setup-models';
 import type CollectionProgress from '~/server/models/vo/collection-progress';
 import type CollectionProgress from '~/server/models/vo/collection-progress';
+import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import CollectionProgressingStatus from '../../models/vo/collection-progressing-status';
 import CollectionProgressingStatus from '../../models/vo/collection-progressing-status';
@@ -463,7 +464,7 @@ export class ImportService {
    * @param {object} meta meta data from meta.json
    * @param {object} meta meta data from meta.json
    */
    */
   validate(meta) {
   validate(meta) {
-    if (meta.version !== this.crowi.version) {
+    if (meta.version !== getGrowiVersion()) {
       throw new Error('The version of this GROWI and the uploaded GROWI data are not the same');
       throw new Error('The version of this GROWI and the uploaded GROWI data are not the same');
     }
     }
 
 

+ 4 - 2
apps/app/src/server/service/mail.ts

@@ -8,6 +8,8 @@ import loggerFactory from '~/utils/logger';
 import S2sMessage from '../models/vo/s2s-message';
 import S2sMessage from '../models/vo/s2s-message';
 
 
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
+import type { IConfigManagerForApp } from './config-manager';
+import type Crowi from '../crowi';
 
 
 const logger = loggerFactory('growi:service:mail');
 const logger = loggerFactory('growi:service:mail');
 
 
@@ -23,7 +25,7 @@ class MailService implements S2sMessageHandlable {
 
 
   appService!: any;
   appService!: any;
 
 
-  configManager!: any;
+  configManager: IConfigManagerForApp;
 
 
   s2sMessagingService!: any;
   s2sMessagingService!: any;
 
 
@@ -38,7 +40,7 @@ class MailService implements S2sMessageHandlable {
    */
    */
   isMailerSetup = false;
   isMailerSetup = false;
 
 
-  constructor(crowi) {
+  constructor(crowi: Crowi) {
     this.appService = crowi.appService;
     this.appService = crowi.appService;
     this.configManager = crowi.configManager;
     this.configManager = crowi.configManager;
     this.s2sMessagingService = crowi.s2sMessagingService;
     this.s2sMessagingService = crowi.s2sMessagingService;

+ 28 - 22
apps/app/src/server/service/passport.ts

@@ -20,6 +20,7 @@ import S2sMessage from '../models/vo/s2s-message';
 
 
 import { configManager } from './config-manager';
 import { configManager } from './config-manager';
 import type { ConfigKey } from './config-manager/config-definition';
 import type { ConfigKey } from './config-manager/config-definition';
+import { growiInfoService } from './growi-info';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
 
 const logger = loggerFactory('growi:service:PassportService');
 const logger = loggerFactory('growi:service:PassportService');
@@ -449,7 +450,6 @@ class PassportService implements S2sMessageHandlable {
 
 
     this.resetGoogleStrategy();
     this.resetGoogleStrategy();
 
 
-    const { configManager } = this.crowi;
     const isGoogleEnabled = configManager.getConfig('security:passport-google:isEnabled');
     const isGoogleEnabled = configManager.getConfig('security:passport-google:isEnabled');
 
 
     // when disabled
     // when disabled
@@ -463,9 +463,9 @@ class PassportService implements S2sMessageHandlable {
         {
         {
           clientID: configManager.getConfig('security:passport-google:clientId'),
           clientID: configManager.getConfig('security:passport-google:clientId'),
           clientSecret: configManager.getConfig('security:passport-google:clientSecret'),
           clientSecret: configManager.getConfig('security:passport-google:clientSecret'),
-          callbackURL: (this.crowi.appService.getSiteUrl() != null)
-            ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/google/callback') // auto-generated with v3.2.4 and above
-            : configManager.getConfig('security:passport-google:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
+          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,
           skipUserProfile: false,
         },
         },
         (accessToken, refreshToken, profile, done) => {
         (accessToken, refreshToken, profile, done) => {
@@ -497,7 +497,6 @@ class PassportService implements S2sMessageHandlable {
 
 
     this.resetGitHubStrategy();
     this.resetGitHubStrategy();
 
 
-    const { configManager } = this.crowi;
     const isGitHubEnabled = configManager.getConfig('security:passport-github:isEnabled');
     const isGitHubEnabled = configManager.getConfig('security:passport-github:isEnabled');
 
 
     // when disabled
     // when disabled
@@ -511,9 +510,9 @@ class PassportService implements S2sMessageHandlable {
         {
         {
           clientID: configManager.getConfig('security:passport-github:clientId'),
           clientID: configManager.getConfig('security:passport-github:clientId'),
           clientSecret: configManager.getConfig('security:passport-github:clientSecret'),
           clientSecret: configManager.getConfig('security:passport-github:clientSecret'),
-          callbackURL: (this.crowi.appService.getSiteUrl() != null)
-            ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/github/callback') // auto-generated with v3.2.4 and above
-            : configManager.getConfig('security:passport-github:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
+          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,
           skipUserProfile: false,
         },
         },
         (accessToken, refreshToken, profile, done) => {
         (accessToken, refreshToken, profile, done) => {
@@ -545,7 +544,6 @@ class PassportService implements S2sMessageHandlable {
 
 
     this.resetOidcStrategy();
     this.resetOidcStrategy();
 
 
-    const { configManager } = this.crowi;
     const isOidcEnabled = configManager.getConfig('security:passport-oidc:isEnabled');
     const isOidcEnabled = configManager.getConfig('security:passport-oidc:isEnabled');
 
 
     // when disabled
     // when disabled
@@ -567,13 +565,13 @@ class PassportService implements S2sMessageHandlable {
     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 clientId = configManager.getConfig('security:passport-oidc:clientId');
     const clientSecret = configManager.getConfig('security:passport-oidc:clientSecret');
     const clientSecret = configManager.getConfig('security:passport-oidc:clientSecret');
-    const redirectUri = (configManager.getConfig('app:siteUrl') != null)
-      ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/oidc/callback')
-      : configManager.getConfig('security:passport-oidc:callbackUrl'); // DEPRECATED: backward compatible with v3.2.3 and below
+    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
     // Prevent request timeout error on app init
     const oidcIssuer = await this.getOIDCIssuerInstance(issuerHost);
     const oidcIssuer = await this.getOIDCIssuerInstance(issuerHost);
-    if (oidcIssuer != null) {
+    if (clientId != null && oidcIssuer != null) {
       const oidcIssuerMetadata = oidcIssuer.metadata;
       const oidcIssuerMetadata = oidcIssuer.metadata;
 
 
       logger.debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
       logger.debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
@@ -715,15 +713,17 @@ class PassportService implements S2sMessageHandlable {
    * @param issuerHost string
    * @param issuerHost string
    * @returns instance of OIDCIssuer
    * @returns instance of OIDCIssuer
    */
    */
-  async getOIDCIssuerInstance(issuerHost: string): Promise<void | OIDCIssuer> {
-    const OIDC_TIMEOUT_MULTIPLIER = await configManager.getConfig('security:passport-oidc:timeoutMultiplier');
-    const OIDC_DISCOVERY_RETRIES = await configManager.getConfig('security:passport-oidc:discoveryRetries');
-    const OIDC_ISSUER_TIMEOUT_OPTION = await configManager.getConfig('security:passport-oidc:oidcIssuerTimeoutOption');
-    const oidcIssuerHostReady = await 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) {
     if (!oidcIssuerHostReady) {
       logger.error('OidcStrategy: setup failed');
       logger.error('OidcStrategy: setup failed');
       return;
       return;
     }
     }
+
     const metadataURL = this.getOIDCMetadataURL(issuerHost);
     const metadataURL = this.getOIDCMetadataURL(issuerHost);
     const oidcIssuer = await pRetry(async() => {
     const oidcIssuer = await pRetry(async() => {
       return OIDCIssuer.discover(metadataURL);
       return OIDCIssuer.discover(metadataURL);
@@ -751,7 +751,6 @@ class PassportService implements S2sMessageHandlable {
 
 
     this.resetSamlStrategy();
     this.resetSamlStrategy();
 
 
-    const { configManager } = this.crowi;
     const isSamlEnabled = configManager.getConfig('security:passport-saml:isEnabled');
     const isSamlEnabled = configManager.getConfig('security:passport-saml:isEnabled');
 
 
     // when disabled
     // when disabled
@@ -760,15 +759,22 @@ class PassportService implements S2sMessageHandlable {
     }
     }
 
 
     logger.debug('SamlStrategy: setting up..');
     logger.debug('SamlStrategy: setting up..');
+
+    const cert = configManager.getConfig('security:passport-saml:cert');
+    if (cert == null) {
+      logger.warn('SamlStrategy: cert is not set. setup is skipped.');
+      return;
+    }
+
     passport.use(
     passport.use(
       new SamlStrategy(
       new SamlStrategy(
         {
         {
           entryPoint: configManager.getConfig('security:passport-saml:entryPoint'),
           entryPoint: configManager.getConfig('security:passport-saml:entryPoint'),
-          callbackUrl: (this.crowi.appService.getSiteUrl() != null)
-            ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/saml/callback') // auto-generated with v3.2.4 and above
+          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
             : configManager.getConfig('security:passport-saml:callbackUrl'), // DEPRECATED: backward compatible with v3.2.3 and below
           issuer: configManager.getConfig('security:passport-saml:issuer'),
           issuer: configManager.getConfig('security:passport-saml:issuer'),
-          cert: configManager.getConfig('security:passport-saml:cert'),
+          cert,
           disableRequestedAuthnContext: true,
           disableRequestedAuthnContext: true,
         },
         },
         (profile: Profile, done: VerifiedCallback) => {
         (profile: Profile, done: VerifiedCallback) => {

+ 5 - 4
apps/app/src/server/service/s2s-messaging/index.ts

@@ -1,6 +1,7 @@
+import type Crowi from '~/server/crowi';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { S2sMessagingService } from './base';
+import type { S2sMessagingService } from './base';
 
 
 const logger = loggerFactory('growi:service:s2s-messaging:S2sMessagingServiceFactory');
 const logger = loggerFactory('growi:service:s2s-messaging:S2sMessagingServiceFactory');
 
 
@@ -42,7 +43,7 @@ class S2sMessagingServiceFactory {
 
 
   delegator!: S2sMessagingService;
   delegator!: S2sMessagingService;
 
 
-  initializeDelegator(crowi) {
+  initializeDelegator(crowi: Crowi) {
     const type = crowi.configManager.getConfig('s2sMessagingPubsub:serverType');
     const type = crowi.configManager.getConfig('s2sMessagingPubsub:serverType');
 
 
     if (type == null) {
     if (type == null) {
@@ -63,7 +64,7 @@ class S2sMessagingServiceFactory {
     }
     }
   }
   }
 
 
-  getDelegator(crowi) {
+  getDelegator(crowi: Crowi) {
     if (this.delegator == null) {
     if (this.delegator == null) {
       this.initializeDelegator(crowi);
       this.initializeDelegator(crowi);
     }
     }
@@ -74,6 +75,6 @@ class S2sMessagingServiceFactory {
 
 
 const factory = new S2sMessagingServiceFactory();
 const factory = new S2sMessagingServiceFactory();
 
 
-module.exports = (crowi) => {
+module.exports = (crowi: Crowi) => {
   return factory.getDelegator(crowi);
   return factory.getDelegator(crowi);
 };
 };

+ 0 - 2
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -53,8 +53,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
 
   name!: SearchDelegatorName.DEFAULT;
   name!: SearchDelegatorName.DEFAULT;
 
 
-  configManager!: any;
-
   socketIoService!: any;
   socketIoService!: any;
 
 
   isElasticsearchV7: boolean;
   isElasticsearchV7: boolean;

+ 7 - 1
apps/app/src/server/service/slack-command-handler/create-page-service.js

@@ -1,9 +1,12 @@
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 import { reshapeContentsBody } from '@growi/slack/dist/utils/reshape-contents-body';
 import { reshapeContentsBody } from '@growi/slack/dist/utils/reshape-contents-body';
 
 
+import Crowi from '~/server/crowi';
 import { generalXssFilter } from '~/services/general-xss-filter';
 import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { growiInfoService } from '../growi-info';
+
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:service:CreatePageService');
 const logger = loggerFactory('growi:service:CreatePageService');
 
 
@@ -12,6 +15,9 @@ const mongoose = require('mongoose');
 
 
 class CreatePageService {
 class CreatePageService {
 
 
+  /** @type {import('~/server/crowi').default} Crowi instance */
+  crowi;
+
   constructor(crowi) {
   constructor(crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
@@ -29,7 +35,7 @@ class CreatePageService {
     const page = await this.crowi.pageService.create(normalizedPath, reshapedContentsBody, userOrDummyUser, {});
     const page = await this.crowi.pageService.create(normalizedPath, reshapedContentsBody, userOrDummyUser, {});
 
 
     // Send a message when page creation is complete
     // Send a message when page creation is complete
-    const growiUri = this.crowi.appService.getSiteUrl();
+    const growiUri = growiInfoService.getSiteUrl();
     await respondUtil.respond({
     await respondUtil.respond({
       text: 'Page has been created',
       text: 'Page has been created',
       blocks: [
       blocks: [

+ 4 - 1
apps/app/src/server/service/slack-command-handler/help.js

@@ -5,13 +5,16 @@
 
 
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 
 
+import { growiInfoService } from '../growi-info';
+
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const BaseSlackCommandHandler = require('./slack-command-handler');
   const BaseSlackCommandHandler = require('./slack-command-handler');
   const handler = new BaseSlackCommandHandler();
   const handler = new BaseSlackCommandHandler();
 
 
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
     const appTitle = crowi.appService.getAppTitle();
     const appTitle = crowi.appService.getAppTitle();
-    const appSiteUrl = crowi.appService.getSiteUrl();
+    const appSiteUrl = growiInfoService.getSiteUrl();
     // adjust spacing
     // adjust spacing
     let message = `*Help* (*${appTitle}* at ${appSiteUrl})\n\n`;
     let message = `*Help* (*${appTitle}* at ${appSiteUrl})\n\n`;
     message += 'Usage:     `/growi [command] [args]`\n\n';
     message += 'Usage:     `/growi [command] [args]`\n\n';

+ 4 - 2
apps/app/src/server/service/slack-command-handler/search.js

@@ -5,6 +5,8 @@ import { generateLastUpdateMrkdwn } from '@growi/slack/dist/utils/generate-last-
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { growiInfoService } from '../growi-info';
+
 
 
 const logger = loggerFactory('growi:service:SlackCommandHandler:search');
 const logger = loggerFactory('growi:service:SlackCommandHandler:search');
 
 
@@ -56,7 +58,7 @@ module.exports = (crowi) => {
   }
   }
 
 
   function buildRespondBodyForSearchResult(searchResult, growiCommandArgs) {
   function buildRespondBodyForSearchResult(searchResult, growiCommandArgs) {
-    const appUrl = crowi.appService.getSiteUrl();
+    const appUrl = growiInfoService.getSiteUrl();
     const appTitle = crowi.appService.getAppTitle();
     const appTitle = crowi.appService.getAppTitle();
 
 
     const {
     const {
@@ -237,7 +239,7 @@ module.exports = (crowi) => {
   handler.shareSinglePageResult = async function(client, payload, interactionPayloadAccessor, respondUtil) {
   handler.shareSinglePageResult = async function(client, payload, interactionPayloadAccessor, respondUtil) {
     const { user } = payload;
     const { user } = payload;
 
 
-    const appUrl = crowi.appService.getSiteUrl();
+    const appUrl = growiInfoService.getSiteUrl();
     const appTitle = crowi.appService.getAppTitle();
     const appTitle = crowi.appService.getAppTitle();
 
 
     const value = interactionPayloadAccessor.firstAction()?.value; // shareSinglePage action must have button action
     const value = interactionPayloadAccessor.firstAction()?.value; // shareSinglePage action must have button action

+ 4 - 3
apps/app/src/server/service/slack-event-handler/link-shared.ts

@@ -11,8 +11,9 @@ import loggerFactory from '~/utils/logger';
 import type {
 import type {
   DataForUnfurl, PublicData, UnfurlEventLink, UnfurlRequestEvent,
   DataForUnfurl, PublicData, UnfurlEventLink, UnfurlRequestEvent,
 } from '../../interfaces/slack-integration/link-shared-unfurl';
 } from '../../interfaces/slack-integration/link-shared-unfurl';
+import { growiInfoService } from '../growi-info';
 
 
-import { SlackEventHandler } from './base-event-handler';
+import type { SlackEventHandler } from './base-event-handler';
 
 
 const logger = loggerFactory('growi:service:SlackEventHandler:link-shared');
 const logger = loggerFactory('growi:service:SlackEventHandler:link-shared');
 
 
@@ -38,7 +39,7 @@ export class LinkSharedEventHandler implements SlackEventHandler<UnfurlRequestEv
 
 
   async handleEvent(client: WebClient, growiBotEvent: GrowiBotEvent<UnfurlRequestEvent>, data?: {origin: string}): Promise<void> {
   async handleEvent(client: WebClient, growiBotEvent: GrowiBotEvent<UnfurlRequestEvent>, data?: {origin: string}): Promise<void> {
     const { event } = growiBotEvent;
     const { event } = growiBotEvent;
-    const origin = data?.origin || this.crowi.appService.getSiteUrl();
+    const origin = data?.origin || growiInfoService.getSiteUrl();
     const { channel, message_ts: ts, links } = event;
     const { channel, message_ts: ts, links } = event;
 
 
     let unfurlData: DataForUnfurl[];
     let unfurlData: DataForUnfurl[];
@@ -90,7 +91,7 @@ export class LinkSharedEventHandler implements SlackEventHandler<UnfurlRequestEv
     const { pageBody: text, updatedAt } = body;
     const { pageBody: text, updatedAt } = body;
 
 
     const appTitle = this.crowi.appService.getAppTitle();
     const appTitle = this.crowi.appService.getAppTitle();
-    const siteUrl = this.crowi.appService.getSiteUrl();
+    const siteUrl = growiInfoService.getSiteUrl();
 
 
     const attachment: MessageAttachment = {
     const attachment: MessageAttachment = {
       title: body.path,
       title: body.path,

+ 2 - 1
apps/app/src/server/service/user-notification/index.ts

@@ -7,6 +7,7 @@ import {
   prepareSlackMessageForPage,
   prepareSlackMessageForPage,
   prepareSlackMessageForComment,
   prepareSlackMessageForComment,
 } from '../../util/slack';
 } from '../../util/slack';
+import { growiInfoService } from '../growi-info';
 
 
 /**
 /**
  * service class of UserNotification
  * service class of UserNotification
@@ -51,7 +52,7 @@ export class UserNotificationService {
     const slackChannels: (string|null)[] = toArrayFromCsv(slackChannelsStr);
     const slackChannels: (string|null)[] = toArrayFromCsv(slackChannelsStr);
 
 
     const appTitle = appService.getAppTitle();
     const appTitle = appService.getAppTitle();
-    const siteUrl = appService.getSiteUrl();
+    const siteUrl = growiInfoService.getSiteUrl();
 
 
     const promises = slackChannels.map(async(chan) => {
     const promises = slackChannels.map(async(chan) => {
       let messageObj;
       let messageObj;

+ 2 - 0
apps/app/src/server/util/importer.js

@@ -1,4 +1,5 @@
 import Esa from 'esa-node';
 import Esa from 'esa-node';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:util:importer');
 const logger = loggerFactory('growi:util:importer');
@@ -9,6 +10,7 @@ const logger = loggerFactory('growi:util:importer');
 
 
 /* eslint-disable no-use-before-define */
 /* eslint-disable no-use-before-define */
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const createGrowiPages = require('./createGrowiPagesFromImports')(crowi);
   const createGrowiPages = require('./createGrowiPagesFromImports')(crowi);
   const restQiitaAPIService = crowi.getRestQiitaAPIService();
   const restQiitaAPIService = crowi.getRestQiitaAPIService();

+ 5 - 0
apps/app/src/utils/growi-version.ts

@@ -0,0 +1,5 @@
+import pkg from '^/package.json';
+
+export const getGrowiVersion = (): string => {
+  return pkg.version;
+};

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

@@ -279,10 +279,17 @@ describe('QuestionnaireCronService', () => {
       answeredAt: new Date(),
       answeredAt: new Date(),
       growiInfo: {
       growiInfo: {
         version: '1.0',
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        appSiteUrl: 'https://example.com',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
         deploymentType: GrowiDeploymentType.others,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         additionalInfo: {
         additionalInfo: {
           installedAt: new Date('2000-01-01'),
           installedAt: new Date('2000-01-01'),
           installedAtByOldestUser: new Date('2020-01-01'),
           installedAtByOldestUser: new Date('2020-01-01'),
@@ -307,7 +314,9 @@ describe('QuestionnaireCronService', () => {
       answeredAt: new Date(),
       answeredAt: new Date(),
       growiInfo: {
       growiInfo: {
         version: '1.0',
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        appSiteUrl: 'https://example.com',
+        appSiteUrlHashed: 'hashed',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
         deploymentType: GrowiDeploymentType.others,
@@ -315,6 +324,12 @@ describe('QuestionnaireCronService', () => {
         installedAtByOldestUser: new Date('2020-01-01'),
         installedAtByOldestUser: new Date('2020-01-01'),
         currentUsersCount: 100,
         currentUsersCount: 100,
         currentActiveUsersCount: 50,
         currentActiveUsersCount: 50,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         attachmentType: AttachmentMethodType.aws,
         attachmentType: AttachmentMethodType.aws,
       },
       },
       userInfo: {
       userInfo: {
@@ -338,10 +353,17 @@ describe('QuestionnaireCronService', () => {
       commentText: 'answer text',
       commentText: 'answer text',
       growiInfo: {
       growiInfo: {
         version: '1.0',
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        appSiteUrl: 'https://example.com',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
         deploymentType: GrowiDeploymentType.others,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         additionalInfo: {
         additionalInfo: {
           installedAt: new Date('2000-01-01'),
           installedAt: new Date('2000-01-01'),
           installedAtByOldestUser: new Date('2020-01-01'),
           installedAtByOldestUser: new Date('2020-01-01'),
@@ -362,10 +384,18 @@ describe('QuestionnaireCronService', () => {
       commentText: 'answer text',
       commentText: 'answer text',
       growiInfo: {
       growiInfo: {
         version: '1.0',
         version: '1.0',
-        appSiteUrlHashed: 'c83e8d2a1aa87b2a3f90561be372ca523bb931e2d00013c1d204879621a25b90',
+        appSiteUrl: 'https://example.com',
+        appSiteUrlHashed: 'hashed',
+        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
         type: GrowiServiceType.cloud,
         type: GrowiServiceType.cloud,
         wikiType: GrowiWikiType.open,
         wikiType: GrowiWikiType.open,
         deploymentType: GrowiDeploymentType.others,
         deploymentType: GrowiDeploymentType.others,
+        osInfo: {
+          type: 'Linux',
+          platform: 'linux',
+          arch: 'x64',
+          totalmem: 8589934592,
+        },
         // legacy properties
         // legacy properties
         installedAt: new Date('2000-01-01'),
         installedAt: new Date('2000-01-01'),
         installedAtByOldestUser: new Date('2020-01-01'),
         installedAtByOldestUser: new Date('2020-01-01'),
@@ -406,6 +436,7 @@ describe('QuestionnaireCronService', () => {
     const savedOrders = await QuestionnaireOrder.find()
     const savedOrders = await QuestionnaireOrder.find()
       .select('-condition._id -questions._id -questions.createdAt -questions.updatedAt')
       .select('-condition._id -questions._id -questions.createdAt -questions.updatedAt')
       .sort({ _id: 1 });
       .sort({ _id: 1 });
+
     expect(JSON.parse(JSON.stringify(savedOrders))).toEqual([
     expect(JSON.parse(JSON.stringify(savedOrders))).toEqual([
       {
       {
         _id: '63a8354837e7aa378e16f0b1',
         _id: '63a8354837e7aa378e16f0b1',

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

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

Разница между файлами не показана из-за своего большого размера
+ 393 - 360
pnpm-lock.yaml


Некоторые файлы не были показаны из-за большого количества измененных файлов