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

Merge pull request #8810 from weseek/feat/opentelemetry

feat: Support OpenTelemetry
Yuki Takei 1 год назад
Родитель
Сommit
f2926ace3b
100 измененных файлов с 1349 добавлено и 566 удалено
  1. 5 0
      .changeset/clever-impalas-dress.md
  2. 1 0
      .devcontainer/.gitignore
  3. 8 0
      .devcontainer/compose.yml
  4. 4 0
      apps/app/.env.development
  5. 4 0
      apps/app/.env.production
  6. 10 2
      apps/app/package.json
  7. 0 3
      apps/app/public/static/locales/en_US/admin.json
  8. 0 3
      apps/app/public/static/locales/fr_FR/admin.json
  9. 0 3
      apps/app/public/static/locales/ja_JP/admin.json
  10. 0 3
      apps/app/public/static/locales/zh_CN/admin.json
  11. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx
  12. 0 36
      apps/app/src/client/components/Admin/Security/FacebookSecuritySetting.jsx
  13. 0 8
      apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx
  14. 2 1
      apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx
  15. 2 3
      apps/app/src/client/components/LoginForm/ExternalAuthButton.tsx
  16. 0 6
      apps/app/src/client/components/LoginForm/LoginForm.module.scss
  17. 1 1
      apps/app/src/client/components/LoginForm/LoginForm.tsx
  18. 0 6
      apps/app/src/client/components/Me/AssociateModal.tsx
  19. 3 2
      apps/app/src/client/components/Me/DisassociateModal.tsx
  20. 1 1
      apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx
  21. 28 28
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts
  22. 7 6
      apps/app/src/features/external-user-group/server/service/external-user-group-sync.ts
  23. 2 1
      apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.integ.ts
  24. 14 13
      apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.ts
  25. 12 10
      apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts
  26. 2 2
      apps/app/src/features/openai/server/routes/middlewares/certify-ai-service.ts
  27. 16 5
      apps/app/src/features/openai/server/services/assistant/assistant.ts
  28. 1 1
      apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts
  29. 1 1
      apps/app/src/features/openai/server/services/client.ts
  30. 4 4
      apps/app/src/features/openai/server/services/cron/thread-deletion-cron.ts
  31. 4 4
      apps/app/src/features/openai/server/services/cron/vector-store-file-deletion-cron.ts
  32. 1 1
      apps/app/src/features/openai/server/services/is-ai-enabled.ts
  33. 3 3
      apps/app/src/features/openai/server/services/openai.ts
  34. 1 0
      apps/app/src/features/opentelemetry/server/index.ts
  35. 76 0
      apps/app/src/features/opentelemetry/server/logger.ts
  36. 67 0
      apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts
  37. 103 0
      apps/app/src/features/opentelemetry/server/node-sdk.ts
  38. 2 2
      apps/app/src/features/questionnaire/interfaces/condition.ts
  39. 18 0
      apps/app/src/features/questionnaire/interfaces/growi-app-info.ts
  40. 0 58
      apps/app/src/features/questionnaire/interfaces/growi-info.ts
  41. 16 3
      apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts
  42. 14 4
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts
  43. 3 2
      apps/app/src/features/questionnaire/server/models/questionnaire-order.ts
  44. 2 2
      apps/app/src/features/questionnaire/server/models/schema/condition.ts
  45. 26 10
      apps/app/src/features/questionnaire/server/models/schema/growi-info.ts
  46. 20 11
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  47. 22 11
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts
  48. 301 0
      apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts
  49. 8 70
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  50. 9 6
      apps/app/src/features/questionnaire/server/util/condition.ts
  51. 125 0
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts
  52. 40 0
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts
  53. 2 1
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  54. 1 0
      apps/app/src/interfaces/activity.ts
  55. 13 1
      apps/app/src/interfaces/attachment.ts
  56. 3 2
      apps/app/src/interfaces/crowi-request.ts
  57. 9 0
      apps/app/src/interfaces/external-auth-provider.ts
  58. 38 0
      apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js
  59. 24 0
      apps/app/src/migrations/19700101000000-foremost-1010-20250109000000-generate-service-instance-id.js
  60. 2 6
      apps/app/src/migrations/20180927102719-init-serverurl.js
  61. 6 6
      apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  62. 3 6
      apps/app/src/migrations/20190618104011-add-config-app-installed.js
  63. 0 1
      apps/app/src/migrations/20200512005851-remove-behavior-type.js
  64. 0 1
      apps/app/src/migrations/20200827045151-remove-layout-setting.js
  65. 0 2
      apps/app/src/migrations/20200828024025-copy-aws-setting.js
  66. 0 3
      apps/app/src/migrations/20200901034313-update-mail-transmission.js
  67. 0 1
      apps/app/src/migrations/20200903080025-remove-timeline-type.js.js
  68. 0 5
      apps/app/src/migrations/20220311011114-convert-page-delete-config.js
  69. 0 1
      apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js
  70. 0 1
      apps/app/src/migrations/20230213090921-remove-presentation-configurations.js
  71. 48 43
      apps/app/src/pages/[[...path]].page.tsx
  72. 18 16
      apps/app/src/pages/_private-legacy-pages.page.tsx
  73. 19 17
      apps/app/src/pages/_search.page.tsx
  74. 1 1
      apps/app/src/pages/admin/ai-integration.page.tsx
  75. 2 2
      apps/app/src/pages/admin/audit-log.page.tsx
  76. 3 2
      apps/app/src/pages/admin/customize.page.tsx
  77. 1 1
      apps/app/src/pages/admin/data-transfer.page.tsx
  78. 5 5
      apps/app/src/pages/admin/index.page.tsx
  79. 2 2
      apps/app/src/pages/admin/security.page.tsx
  80. 2 2
      apps/app/src/pages/admin/slack-integration.page.tsx
  81. 1 1
      apps/app/src/pages/installer.page.tsx
  82. 11 12
      apps/app/src/pages/login/index.page.tsx
  83. 19 17
      apps/app/src/pages/me/[[...path]].page.tsx
  84. 28 23
      apps/app/src/pages/share/[[...path]].page.tsx
  85. 3 3
      apps/app/src/pages/tags.page.tsx
  86. 4 4
      apps/app/src/pages/trash.page.tsx
  87. 2 2
      apps/app/src/pages/user-activation.page.tsx
  88. 9 8
      apps/app/src/pages/utils/commons.ts
  89. 8 4
      apps/app/src/server/app.ts
  90. 1 2
      apps/app/src/server/crowi/dev.js
  91. 5 4
      apps/app/src/server/crowi/express-init.js
  92. 55 17
      apps/app/src/server/crowi/index.js
  93. 2 1
      apps/app/src/server/events/admin.js
  94. 1 0
      apps/app/src/server/events/bookmark.js
  95. 1 0
      apps/app/src/server/events/page.js
  96. 2 1
      apps/app/src/server/events/tag.js
  97. 2 2
      apps/app/src/server/middlewares/add-activity.ts
  98. 1 0
      apps/app/src/server/middlewares/admin-required.js
  99. 1 0
      apps/app/src/server/middlewares/application-installed.js
  100. 1 0
      apps/app/src/server/middlewares/application-not-installed.js

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

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

+ 1 - 0
.devcontainer/.gitignore

@@ -0,0 +1 @@
+.env

+ 8 - 0
.devcontainer/compose.yml

@@ -9,6 +9,9 @@ services:
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../share:/workspace/share:delegated
       - ../../share:/workspace/share:delegated
     tty: true
     tty: true
+    networks:
+    - default
+    - opentelemetry-collector-dev-setup_default
 
 
   mongo:
   mongo:
     image: mongo:6.0
     image: mongo:6.0
@@ -45,3 +48,8 @@ volumes:
   pnpm-store:
   pnpm-store:
   node_modules:
   node_modules:
   buildcache_app:
   buildcache_app:
+
+networks:
+  default:
+  opentelemetry-collector-dev-setup_default:
+    external: ${OPENTELEMETRY_COLLECTOR_DEV_ENABLED:-false}

+ 4 - 0
apps/app/.env.development

@@ -30,3 +30,7 @@ QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # AUDIT_LOG_ACTION_GROUP_SIZE=SMALL
 # AUDIT_LOG_ACTION_GROUP_SIZE=SMALL
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
+
+# OpenTelemetry Configuration
+OPENTELEMETRY_ENABLED=false
+OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

+ 4 - 0
apps/app/.env.production

@@ -4,3 +4,7 @@
 ##
 ##
 FORMAT_NODE_LOG=false
 FORMAT_NODE_LOG=false
 MIGRATIONS_DIR=dist/migrations/
 MIGRATIONS_DIR=dist/migrations/
+
+# OpenTelemetry Configuration
+OTEL_TRACES_SAMPLER_ARG=0.1
+

+ 10 - 2
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.1.10-RC.0",
+  "version": "7.2.0-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": "true",
   "private": "true",
   "scripts": {
   "scripts": {
@@ -21,7 +21,6 @@
     "dev:pre:styles": "pnpm run pre:styles --mode dev",
     "dev:pre:styles": "pnpm run pre:styles --mode dev",
     "dev:migrate-mongo": "cross-env NODE_ENV=development pnpm run ts-node node_modules/migrate-mongo/bin/migrate-mongo",
     "dev:migrate-mongo": "cross-env NODE_ENV=development pnpm run ts-node node_modules/migrate-mongo/bin/migrate-mongo",
     "dev:migrate": "pnpm run dev:migrate:status > tmp/cache/migration-status.out && pnpm run dev:migrate:up",
     "dev:migrate": "pnpm run dev:migrate:status > tmp/cache/migration-status.out && pnpm run dev:migrate:up",
-    "dev:migrate:create": "pnpm run dev:migrate-mongo create -f config/migrate-mongo-config.js",
     "dev:migrate:status": "pnpm run dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "dev:migrate:status": "pnpm run dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "dev:migrate:up": "pnpm run dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:up": "pnpm run dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:down": "pnpm run dev:migrate-mongo down -f config/migrate-mongo-config.js",
     "dev:migrate:down": "pnpm run dev:migrate-mongo down -f config/migrate-mongo-config.js",
@@ -84,6 +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.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",

+ 0 - 3
apps/app/public/static/locales/en_US/admin.json

@@ -184,9 +184,6 @@
         "register_5": "Copy and paste your ClientID and Client Secret above",
         "register_5": "Copy and paste your ClientID and Client Secret above",
         "updated_google": "Succeeded to update Google OAuth setting"
         "updated_google": "Succeeded to update Google OAuth setting"
       },
       },
-      "Facebook": {
-        "name": "Facebook OAuth"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "Enable GitHub OAuth",
         "enable_github": "Enable GitHub OAuth",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",

+ 0 - 3
apps/app/public/static/locales/fr_FR/admin.json

@@ -184,9 +184,6 @@
         "register_5": "Copier l'ID client et Secret client ci-dessus",
         "register_5": "Copier l'ID client et Secret client ci-dessus",
         "updated_google": "Paramètres mis à jour"
         "updated_google": "Paramètres mis à jour"
       },
       },
-      "Facebook": {
-        "name": "Facebook OAuth"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "Activer GitHub OAuth",
         "enable_github": "Activer GitHub OAuth",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",

+ 0 - 3
apps/app/public/static/locales/ja_JP/admin.json

@@ -193,9 +193,6 @@
         "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
         "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
         "updated_google": "Google OAuth を更新しました"
         "updated_google": "Google OAuth を更新しました"
       },
       },
-      "Facebook": {
-        "name": "Facebook OAuth"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "GitHub OAuth を有効にする",
         "enable_github": "GitHub OAuth を有効にする",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",

+ 0 - 3
apps/app/public/static/locales/zh_CN/admin.json

@@ -193,9 +193,6 @@
 				"register_5": "Copy and paste your ClientID and Client Secret above",
 				"register_5": "Copy and paste your ClientID and Client Secret above",
 				"updated_google": "Succeeded to update Google OAuth setting"
 				"updated_google": "Succeeded to update Google OAuth setting"
 			},
 			},
-			"Facebook": {
-				"name": "Facebook OAuth"
-			},
 			"GitHub": {
 			"GitHub": {
 				"enable_github": "Enable GitHub OAuth",
 				"enable_github": "Enable GitHub OAuth",
 				"name": "GitHub OAuth",
 				"name": "GitHub OAuth",

+ 1 - 1
apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx

@@ -16,7 +16,7 @@ export const CustomizeTitle: FC = () => {
 
 
   const { data: customizeTitle } = useCustomizeTitle();
   const { data: customizeTitle } = useCustomizeTitle();
 
 
-  const [currentCustomizeTitle, setCrrentCustomizeTitle] = useState(customizeTitle);
+  const [currentCustomizeTitle, setCrrentCustomizeTitle] = useState(customizeTitle ?? '');
 
 
   const onClickSubmit = async() => {
   const onClickSubmit = async() => {
     try {
     try {

+ 0 - 36
apps/app/src/client/components/Admin/Security/FacebookSecuritySetting.jsx

@@ -1,36 +0,0 @@
-import React from 'react';
-
-import { withTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-
-class FacebookSecurityManagement extends React.Component {
-
-  render() {
-    const { t } = this.props;
-    return (
-      <>
-        <h2 className="alert-anchor border-bottom">
-          Facebook OAuth { t('admin:security_settings.configuration') }
-        </h2>
-
-        <p className="card custom-card">(TBD)</p>
-      </>
-    );
-  }
-
-}
-
-
-FacebookSecurityManagement.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
-};
-
-const FacebookSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
-
-export default withTranslation()(FacebookSecurityManagementWrapper);

+ 0 - 8
apps/app/src/client/components/Admin/Security/SecurityManagementContents.jsx

@@ -6,7 +6,6 @@ import { TabContent, TabPane } from 'reactstrap';
 
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 import CustomNav from '../../CustomNavigation/CustomNav';
 
 
-// import FacebookSecuritySetting from './FacebookSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
@@ -53,10 +52,6 @@ const SecurityManagementContents = () => {
         Icon: () => <span className="growi-custom-icons align-bottom">github</span>,
         Icon: () => <span className="growi-custom-icons align-bottom">github</span>,
         i18n: 'GitHub',
         i18n: 'GitHub',
       },
       },
-      // passport_facebook: {
-      //   Icon: () => <span className="growi-custom-icons align-bottom">facebook</span>,
-      //   i18n: '(TBD) Facebook',
-      // },
     };
     };
   }, []);
   }, []);
 
 
@@ -114,9 +109,6 @@ const SecurityManagementContents = () => {
           <TabPane tabId="passport_github">
           <TabPane tabId="passport_github">
             {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
             {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
           </TabPane>
           </TabPane>
-          {/* <TabPane tabId="passport_facebook">
-            {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
-          </TabPane> */}
         </TabContent>
         </TabContent>
       </div>
       </div>
     </div>
     </div>

+ 2 - 1
apps/app/src/client/components/Admin/Users/ExternalAccountTable.tsx

@@ -6,6 +6,7 @@ import { useTranslation } from 'next-i18next';
 
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
@@ -76,7 +77,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>
-          { adminExternalAccountsContainer.state.externalAccounts.map((ea: IAdminExternalAccount) => {
+          { adminExternalAccountsContainer.state.externalAccounts.map((ea: IAdminExternalAccount<IExternalAuthProviderType>) => {
             return (
             return (
               <tr key={ea._id}>
               <tr key={ea._id}>
                 <td><span>{ea.providerType}</span></td>
                 <td><span>{ea.providerType}</span></td>

+ 2 - 3
apps/app/src/client/components/LoginForm/ExternalAuthButton.tsx

@@ -1,12 +1,12 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
-import { IExternalAuthProviderType } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
+
 const authIcon = {
 const authIcon = {
   [IExternalAuthProviderType.google]: <span className="growi-custom-icons align-bottom">google</span>,
   [IExternalAuthProviderType.google]: <span className="growi-custom-icons align-bottom">google</span>,
   [IExternalAuthProviderType.github]: <span className="growi-custom-icons align-bottom">github</span>,
   [IExternalAuthProviderType.github]: <span className="growi-custom-icons align-bottom">github</span>,
-  [IExternalAuthProviderType.facebook]: <span className="growi-custom-icons align-bottom">facebook</span>,
   [IExternalAuthProviderType.oidc]: <span className="growi-custom-icons align-bottom">openid</span>,
   [IExternalAuthProviderType.oidc]: <span className="growi-custom-icons align-bottom">openid</span>,
   [IExternalAuthProviderType.saml]: <span className="material-symbols-outlined align-bottom">key</span>,
   [IExternalAuthProviderType.saml]: <span className="material-symbols-outlined align-bottom">key</span>,
 };
 };
@@ -14,7 +14,6 @@ const authIcon = {
 const authLabel = {
 const authLabel = {
   [IExternalAuthProviderType.google]: 'Google',
   [IExternalAuthProviderType.google]: 'Google',
   [IExternalAuthProviderType.github]: 'GitHub',
   [IExternalAuthProviderType.github]: 'GitHub',
-  [IExternalAuthProviderType.facebook]: 'Facebook',
   [IExternalAuthProviderType.oidc]: 'OIDC',
   [IExternalAuthProviderType.oidc]: 'OIDC',
   [IExternalAuthProviderType.saml]: 'SAML',
   [IExternalAuthProviderType.saml]: 'SAML',
 };
 };

+ 0 - 6
apps/app/src/client/components/LoginForm/LoginForm.module.scss

@@ -82,12 +82,6 @@
       --bs-btn-active-bg: #{rgba(#403D3E, 0.7)};
       --bs-btn-active-bg: #{rgba(#403D3E, 0.7)};
     }
     }
 
 
-    .btn-auth-facebook {
-      --bs-btn-bg: #{rgba(#29487d, 0.4)};
-      --bs-btn-hover-bg: #{rgba(#29487d, 0.9)};
-      --bs-btn-active-bg: #{rgba(#29487d, 0.9)};
-    }
-
     .btn-auth-oidc {
     .btn-auth-oidc {
       --bs-btn-bg: #{rgba(#835B1A, 0.4)};
       --bs-btn-bg: #{rgba(#835B1A, 0.4)};
       --bs-btn-hover-bg: #{rgba(#835B1A, 0.8)};
       --bs-btn-hover-bg: #{rgba(#835B1A, 0.8)};

+ 1 - 1
apps/app/src/client/components/LoginForm/LoginForm.tsx

@@ -2,7 +2,6 @@ import React, {
   useState, useEffect, useCallback,
   useState, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
-import type { IExternalAuthProviderType } from '@growi/core';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -13,6 +12,7 @@ import { useTWithOpt } from '~/client/util/t-with-opt';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
 import type { IErrorV3 } from '~/interfaces/errors/v3-error';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 

+ 0 - 6
apps/app/src/client/components/Me/AssociateModal.tsx

@@ -80,12 +80,6 @@ const AssociateModal = (props: Props): JSX.Element => {
             >
             >
               <span className="growi-custom-icons">google</span> (TBD) Google OAuth
               <span className="growi-custom-icons">google</span> (TBD) Google OAuth
             </NavLink>
             </NavLink>
-            {/* <NavLink
-              className={`${activeTab === 4 ? 'active' : ''} d-flex gap-1 align-items-center`}
-              onClick={() => setActiveTab(4)}
-            >
-              <span className="growi-custom-icons">facebook</span> (TBD) Facebook
-            </NavLink> */}
           </Nav>
           </Nav>
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>
             <TabPane tabId={1}>
             <TabPane tabId={1}>

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

@@ -1,6 +1,6 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
-import type { IExternalAccountHasId } from '@growi/core';
+import type { HasObjectId, IExternalAccount } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Modal,
   Modal,
@@ -10,12 +10,13 @@ import {
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 
 type Props = {
 type Props = {
   isOpen: boolean,
   isOpen: boolean,
   onClose: () => void,
   onClose: () => void,
-  accountForDisassociate: IExternalAccountHasId,
+  accountForDisassociate: IExternalAccount<IExternalAuthProviderType> & HasObjectId,
 }
 }
 
 
 
 

+ 1 - 1
apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx

@@ -42,7 +42,7 @@ const ShareLinkAlert: FC<Props> = (props: Props) => {
   return (
   return (
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
       <span className="material-symbols-outlined me-1">link</span>
       <span className="material-symbols-outlined me-1">link</span>
-      {(expiredAt === null ? <span>{t('page_page.notice.no_deadline')}</span>
+      {(expiredAt == null ? <span>{t('page_page.notice.no_deadline')}</span>
       // eslint-disable-next-line react/no-danger
       // eslint-disable-next-line react/no-danger
         : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
         : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
       )}
       )}

+ 28 - 28
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -30,7 +30,7 @@ interface AuthorizedRequest extends Request {
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
@@ -216,14 +216,14 @@ module.exports = (crowi: Crowi): Router => {
 
 
   router.get('/ldap/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/ldap/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
     const settings = {
     const settings = {
-      ldapGroupSearchBase: configManager?.getConfig('crowi', 'external-user-group:ldap:groupSearchBase'),
-      ldapGroupMembershipAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute'),
-      ldapGroupMembershipAttributeType: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType'),
-      ldapGroupChildGroupAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute'),
-      autoGenerateUserOnLdapGroupSync: configManager?.getConfig('crowi', 'external-user-group:ldap:autoGenerateUserOnGroupSync'),
-      preserveDeletedLdapGroups: configManager?.getConfig('crowi', 'external-user-group:ldap:preserveDeletedGroups'),
-      ldapGroupNameAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute'),
-      ldapGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute'),
+      ldapGroupSearchBase: configManager.getConfig('external-user-group:ldap:groupSearchBase'),
+      ldapGroupMembershipAttribute: configManager.getConfig('external-user-group:ldap:groupMembershipAttribute'),
+      ldapGroupMembershipAttributeType: configManager.getConfig('external-user-group:ldap:groupMembershipAttributeType'),
+      ldapGroupChildGroupAttribute: configManager.getConfig('external-user-group:ldap:groupChildGroupAttribute'),
+      autoGenerateUserOnLdapGroupSync: configManager.getConfig('external-user-group:ldap:autoGenerateUserOnGroupSync'),
+      preserveDeletedLdapGroups: configManager.getConfig('external-user-group:ldap:preserveDeletedGroups'),
+      ldapGroupNameAttribute: configManager.getConfig('external-user-group:ldap:groupNameAttribute'),
+      ldapGroupDescriptionAttribute: configManager.getConfig('external-user-group:ldap:groupDescriptionAttribute'),
     };
     };
 
 
     return res.apiv3(settings);
     return res.apiv3(settings);
@@ -231,14 +231,14 @@ module.exports = (crowi: Crowi): Router => {
 
 
   router.get('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
     const settings = {
     const settings = {
-      keycloakHost: configManager?.getConfig('crowi', 'external-user-group:keycloak:host'),
-      keycloakGroupRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm'),
-      keycloakGroupSyncClientRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm'),
-      keycloakGroupSyncClientID: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID'),
-      keycloakGroupSyncClientSecret: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret'),
-      autoGenerateUserOnKeycloakGroupSync: configManager?.getConfig('crowi', 'external-user-group:keycloak:autoGenerateUserOnGroupSync'),
-      preserveDeletedKeycloakGroups: configManager?.getConfig('crowi', 'external-user-group:keycloak:preserveDeletedGroups'),
-      keycloakGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute'),
+      keycloakHost: configManager.getConfig('external-user-group:keycloak:host'),
+      keycloakGroupRealm: configManager.getConfig('external-user-group:keycloak:groupRealm'),
+      keycloakGroupSyncClientRealm: configManager.getConfig('external-user-group:keycloak:groupSyncClientRealm'),
+      keycloakGroupSyncClientID: configManager.getConfig('external-user-group:keycloak:groupSyncClientID'),
+      keycloakGroupSyncClientSecret: configManager.getConfig('external-user-group:keycloak:groupSyncClientSecret'),
+      autoGenerateUserOnKeycloakGroupSync: configManager.getConfig('external-user-group:keycloak:autoGenerateUserOnGroupSync'),
+      preserveDeletedKeycloakGroups: configManager.getConfig('external-user-group:keycloak:preserveDeletedGroups'),
+      keycloakGroupDescriptionAttribute: configManager.getConfig('external-user-group:keycloak:groupDescriptionAttribute'),
     };
     };
 
 
     return res.apiv3(settings);
     return res.apiv3(settings);
@@ -269,7 +269,7 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
 
 
     try {
     try {
-      await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
+      await configManager.updateConfigs(params, { skipPubsub: true });
       return res.apiv3({}, 204);
       return res.apiv3({}, 204);
     }
     }
     catch (err) {
     catch (err) {
@@ -301,7 +301,7 @@ module.exports = (crowi: Crowi): Router => {
       };
       };
 
 
       try {
       try {
-        await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
+        await configManager.updateConfigs(params, { skipPubsub: true });
         return res.apiv3({}, 204);
         return res.apiv3({}, 204);
       }
       }
       catch (err) {
       catch (err) {
@@ -319,7 +319,7 @@ module.exports = (crowi: Crowi): Router => {
       );
       );
     }
     }
 
 
-    const isLdapEnabled = await configManager.getConfig('crowi', 'security:passport-ldap:isEnabled');
+    const isLdapEnabled = await configManager.getConfig('security:passport-ldap:isEnabled');
     if (!isLdapEnabled) {
     if (!isLdapEnabled) {
       return res.apiv3Err(
       return res.apiv3Err(
         new ErrorV3('Authentication using ldap is not set', 'external_user_group.ldap.auth_not_set'), 422,
         new ErrorV3('Authentication using ldap is not set', 'external_user_group.ldap.auth_not_set'), 422,
@@ -349,25 +349,25 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
 
 
     const getAuthProviderType = () => {
     const getAuthProviderType = () => {
-      let kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
+      let kcHost = configManager.getConfig('external-user-group:keycloak:host');
       if (kcHost?.endsWith('/')) {
       if (kcHost?.endsWith('/')) {
         kcHost = kcHost.slice(0, -1);
         kcHost = kcHost.slice(0, -1);
       }
       }
-      const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
+      const kcGroupRealm = configManager.getConfig('external-user-group:keycloak:groupRealm');
 
 
       // starts with kcHost, contains kcGroupRealm in path
       // starts with kcHost, contains kcGroupRealm in path
       // see: https://regex101.com/r/3ihDmf/1
       // see: https://regex101.com/r/3ihDmf/1
       const regex = new RegExp(`^${kcHost}/.*/${kcGroupRealm}(/|$).*`);
       const regex = new RegExp(`^${kcHost}/.*/${kcGroupRealm}(/|$).*`);
 
 
-      const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
-      const oidcIssuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost');
+      const isOidcEnabled = configManager.getConfig('security:passport-oidc:isEnabled');
+      const oidcIssuerHost = configManager.getConfig('security:passport-oidc:issuerHost');
 
 
-      if (isOidcEnabled && regex.test(oidcIssuerHost)) return 'oidc';
+      if (isOidcEnabled && oidcIssuerHost != null && regex.test(oidcIssuerHost)) return 'oidc';
 
 
-      const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
-      const samlEntryPoint = configManager.getConfig('crowi', 'security:passport-saml:entryPoint');
+      const isSamlEnabled = configManager.getConfig('security:passport-saml:isEnabled');
+      const samlEntryPoint = configManager.getConfig('security:passport-saml:entryPoint');
 
 
-      if (isSamlEnabled && regex.test(samlEntryPoint)) return 'saml';
+      if (isSamlEnabled && samlEntryPoint != null && regex.test(samlEntryPoint)) return 'saml';
 
 
       return null;
       return null;
     };
     };

+ 7 - 6
apps/app/src/features/external-user-group/server/service/external-user-group-sync.ts

@@ -3,19 +3,20 @@ import type { IUserHasId } from '@growi/core';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import ExternalAccount from '~/server/models/external-account';
 import ExternalAccount from '~/server/models/external-account';
 import S2sMessage from '~/server/models/vo/s2s-message';
 import S2sMessage from '~/server/models/vo/s2s-message';
-import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
-import { S2sMessageHandlable } from '~/server/service/s2s-messaging/handlable';
+import type { S2sMessagingService } from '~/server/service/s2s-messaging/base';
+import type { S2sMessageHandlable } from '~/server/service/s2s-messaging/handlable';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
 import { configManager } from '../../../../server/service/config-manager';
 import { configManager } from '../../../../server/service/config-manager';
 import { externalAccountService } from '../../../../server/service/external-account';
 import { externalAccountService } from '../../../../server/service/external-account';
-import {
+import type {
   ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, IExternalUserGroupHasId,
   ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, IExternalUserGroupHasId,
 } from '../../interfaces/external-user-group';
 } from '../../interfaces/external-user-group';
 import ExternalUserGroup from '../models/external-user-group';
 import ExternalUserGroup from '../models/external-user-group';
 import ExternalUserGroupRelation from '../models/external-user-group-relation';
 import ExternalUserGroupRelation from '../models/external-user-group-relation';
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 
 
 const logger = loggerFactory('growi:service:external-user-group-sync-service');
 const logger = loggerFactory('growi:service:external-user-group-sync-service');
 
 
@@ -37,7 +38,7 @@ abstract class ExternalUserGroupSyncService implements S2sMessageHandlable {
 
 
   groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
   groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
 
 
-  authProviderType: string | null; // auth provider type (e.g: ldap, oidc). Has to be set before syncExternalUserGroups execution.
+  authProviderType: IExternalAuthProviderType | null; // auth provider type (e.g: ldap, oidc). Has to be set before syncExternalUserGroups execution.
 
 
   socketIoService: any;
   socketIoService: any;
 
 
@@ -93,7 +94,7 @@ abstract class ExternalUserGroupSyncService implements S2sMessageHandlable {
     if (this.authProviderType == null) throw new Error('auth provider type is not set');
     if (this.authProviderType == null) throw new Error('auth provider type is not set');
     if (this.syncStatus.isExecutingSync) throw new Error('External user group sync is already being executed');
     if (this.syncStatus.isExecutingSync) throw new Error('External user group sync is already being executed');
 
 
-    const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
+    const preserveDeletedLdapGroups = configManager.getConfig(`external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
     const existingExternalUserGroupIds: string[] = [];
     const existingExternalUserGroupIds: string[] = [];
 
 
     const socket = this.socketIoService?.getAdminSocket();
     const socket = this.socketIoService?.getAdminSocket();
@@ -183,7 +184,7 @@ abstract class ExternalUserGroupSyncService implements S2sMessageHandlable {
     const authProviderType = this.authProviderType;
     const authProviderType = this.authProviderType;
     if (authProviderType == null) throw new Error('auth provider type is not set');
     if (authProviderType == null) throw new Error('auth provider type is not set');
 
 
-    const autoGenerateUserOnGroupSync = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
+    const autoGenerateUserOnGroupSync = configManager.getConfig(`external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
 
 
     const getExternalAccount = async() => {
     const getExternalAccount = async() => {
       if (autoGenerateUserOnGroupSync && externalAccountService != null) {
       if (autoGenerateUserOnGroupSync && externalAccountService != null) {

+ 2 - 1
apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.integ.ts

@@ -146,7 +146,8 @@ describe('KeycloakUserGroupSyncService.generateExternalUserGroupTrees', () => {
   };
   };
 
 
   beforeAll(async() => {
   beforeAll(async() => {
-    await configManager.updateConfigsInTheSameNamespace('crowi', configParams, true);
+    await configManager.loadConfigs();
+    await configManager.updateConfigs(configParams, { skipPubsub: true });
     keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(null, null);
     keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(null, null);
     keycloakUserGroupSyncService.init('oidc');
     keycloakUserGroupSyncService.init('oidc');
   });
   });

+ 14 - 13
apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.ts

@@ -1,13 +1,14 @@
 import KeycloakAdminClient from '@keycloak/keycloak-admin-client';
 import KeycloakAdminClient from '@keycloak/keycloak-admin-client';
-import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation';
-import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';
+import type GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation';
+import type UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';
 
 
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
-import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
+import type { S2sMessagingService } from '~/server/service/s2s-messaging/base';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
-import { ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
+import type { ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
+import { ExternalGroupProviderType } from '../../interfaces/external-user-group';
 
 
 import ExternalUserGroupSyncService from './external-user-group-sync';
 import ExternalUserGroupSyncService from './external-user-group-sync';
 
 
@@ -22,9 +23,9 @@ export class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
 
 
   kcAdminClient: KeycloakAdminClient;
   kcAdminClient: KeycloakAdminClient;
 
 
-  realm: string; // realm that contains the groups
+  realm: string | undefined; // realm that contains the groups
 
 
-  groupDescriptionAttribute: string; // attribute to map to group description
+  groupDescriptionAttribute: string | undefined; // attribute to map to group description
 
 
   isInitialized = false;
   isInitialized = false;
 
 
@@ -34,10 +35,10 @@ export class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
   }
   }
 
 
   init(authProviderType: 'oidc' | 'saml'): void {
   init(authProviderType: 'oidc' | 'saml'): void {
-    const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
-    const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
-    const kcGroupSyncClientRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm');
-    const kcGroupDescriptionAttribute = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute');
+    const kcHost = configManager.getConfig('external-user-group:keycloak:host');
+    const kcGroupRealm = configManager.getConfig('external-user-group:keycloak:groupRealm');
+    const kcGroupSyncClientRealm = configManager.getConfig('external-user-group:keycloak:groupSyncClientRealm');
+    const kcGroupDescriptionAttribute = configManager.getConfig('external-user-group:keycloak:groupDescriptionAttribute');
 
 
     this.kcAdminClient = new KeycloakAdminClient({ baseUrl: kcHost, realmName: kcGroupSyncClientRealm });
     this.kcAdminClient = new KeycloakAdminClient({ baseUrl: kcHost, realmName: kcGroupSyncClientRealm });
     this.realm = kcGroupRealm;
     this.realm = kcGroupRealm;
@@ -70,12 +71,12 @@ export class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
    * Authenticate to group sync client using client credentials grant type
    * Authenticate to group sync client using client credentials grant type
    */
    */
   private async auth(): Promise<void> {
   private async auth(): Promise<void> {
-    const kcGroupSyncClientID: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID');
-    const kcGroupSyncClientSecret: string = configManager.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret');
+    const kcGroupSyncClientID = configManager.getConfig('external-user-group:keycloak:groupSyncClientID');
+    const kcGroupSyncClientSecret = configManager.getConfig('external-user-group:keycloak:groupSyncClientSecret');
 
 
     await this.kcAdminClient.auth({
     await this.kcAdminClient.auth({
       grantType: 'client_credentials',
       grantType: 'client_credentials',
-      clientId: kcGroupSyncClientID,
+      clientId: kcGroupSyncClientID ?? '',
       clientSecret: kcGroupSyncClientSecret,
       clientSecret: kcGroupSyncClientSecret,
     });
     });
   }
   }

+ 12 - 10
apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts

@@ -1,12 +1,14 @@
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
-import { ldapService, SearchResultEntry } from '~/server/service/ldap';
-import PassportService from '~/server/service/passport';
-import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
+import type { SearchResultEntry } from '~/server/service/ldap';
+import { ldapService } from '~/server/service/ldap';
+import type PassportService from '~/server/service/passport';
+import type { S2sMessagingService } from '~/server/service/s2s-messaging/base';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
+import type { ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
 import {
 import {
-  ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, LdapGroupMembershipAttributeType,
+  ExternalGroupProviderType, LdapGroupMembershipAttributeType,
 } from '../../interfaces/external-user-group';
 } from '../../interfaces/external-user-group';
 
 
 import ExternalUserGroupSyncService from './external-user-group-sync';
 import ExternalUserGroupSyncService from './external-user-group-sync';
@@ -47,11 +49,11 @@ export class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
   }
   }
 
 
   override async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
   override async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
-    const groupChildGroupAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute');
-    const groupMembershipAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute');
-    const groupNameAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute');
-    const groupDescriptionAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute');
-    const groupBase: string = ldapService.getGroupSearchBase();
+    const groupChildGroupAttribute = configManager.getConfig('external-user-group:ldap:groupChildGroupAttribute');
+    const groupMembershipAttribute = configManager.getConfig('external-user-group:ldap:groupMembershipAttribute');
+    const groupNameAttribute = configManager.getConfig('external-user-group:ldap:groupNameAttribute');
+    const groupDescriptionAttribute = configManager.getConfig('external-user-group:ldap:groupDescriptionAttribute');
+    const groupBase = ldapService.getGroupSearchBase();
 
 
     const groupEntries = await ldapService.searchGroupDir();
     const groupEntries = await ldapService.searchGroupDir();
 
 
@@ -117,7 +119,7 @@ export class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
   }
   }
 
 
   private async getUserInfo(userId: string): Promise<ExternalUserInfo | null> {
   private async getUserInfo(userId: string): Promise<ExternalUserInfo | null> {
-    const groupMembershipAttributeType = configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType');
+    const groupMembershipAttributeType = configManager.getConfig('external-user-group:ldap:groupMembershipAttributeType');
     const attrMapUsername = this.passportService.getLdapAttrNameMappedToUsername();
     const attrMapUsername = this.passportService.getLdapAttrNameMappedToUsername();
     const attrMapName = this.passportService.getLdapAttrNameMappedToName();
     const attrMapName = this.passportService.getLdapAttrNameMappedToName();
     const attrMapMail = this.passportService.getLdapAttrNameMappedToMail();
     const attrMapMail = this.passportService.getLdapAttrNameMappedToMail();

+ 2 - 2
apps/app/src/features/openai/server/routes/middlewares/certify-ai-service.ts

@@ -8,7 +8,7 @@ import { OpenaiServiceTypes } from '../../../interfaces/ai';
 const logger = loggerFactory('growi:middlewares:certify-ai-service');
 const logger = loggerFactory('growi:middlewares:certify-ai-service');
 
 
 export const certifyAiService = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {
 export const certifyAiService = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {
-  const aiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
+  const aiEnabled = configManager.getConfig('app:aiEnabled');
 
 
   if (!aiEnabled) {
   if (!aiEnabled) {
     const message = 'AI_ENABLED is not true';
     const message = 'AI_ENABLED is not true';
@@ -16,7 +16,7 @@ export const certifyAiService = (req: Request, res: Response & { apiv3Err }, nex
     return res.apiv3Err(message, 403);
     return res.apiv3Err(message, 403);
   }
   }
 
 
-  const openaiServiceType = configManager.getConfig('crowi', 'openai:serviceType');
+  const openaiServiceType = configManager.getConfig('openai:serviceType');
   if (openaiServiceType == null || !OpenaiServiceTypes.includes(openaiServiceType)) {
   if (openaiServiceType == null || !OpenaiServiceTypes.includes(openaiServiceType)) {
     const message = 'AI_SERVICE_TYPE is missing or contains an invalid value';
     const message = 'AI_SERVICE_TYPE is missing or contains an invalid value';
     logger.error(message);
     logger.error(message);

+ 16 - 5
apps/app/src/features/openai/server/services/assistant/assistant.ts

@@ -15,9 +15,20 @@ const AssistantDefaultModelMap: Record<AssistantType, OpenAI.Chat.ChatModel> = {
   [AssistantType.CHAT]: 'gpt-4o-mini',
   [AssistantType.CHAT]: 'gpt-4o-mini',
 };
 };
 
 
+const isValidChatModel = (model: string): model is OpenAI.Chat.ChatModel => {
+  return model.startsWith('gpt-');
+};
+
 const getAssistantModelByType = (type: AssistantType): OpenAI.Chat.ChatModel => {
 const getAssistantModelByType = (type: AssistantType): OpenAI.Chat.ChatModel => {
-  const configKey = `openai:assistantModel:${type.toLowerCase()}`;
-  return configManager.getConfig('crowi', configKey) ?? AssistantDefaultModelMap[type];
+  const configValue = type === AssistantType.SEARCH
+    ? undefined // TODO: add the value for 'openai:assistantModel:search' to config-definition.ts
+    : configManager.getConfig('openai:assistantModel:chat');
+
+  if (typeof configValue === 'string' && isValidChatModel(configValue)) {
+    return configValue;
+  }
+
+  return AssistantDefaultModelMap[type];
 };
 };
 
 
 type AssistantType = typeof AssistantType[keyof typeof AssistantType];
 type AssistantType = typeof AssistantType[keyof typeof AssistantType];
@@ -45,7 +56,7 @@ const findAssistantByName = async(assistantName: string): Promise<OpenAI.Beta.As
 };
 };
 
 
 const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Promise<OpenAI.Beta.Assistant> => {
 const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Promise<OpenAI.Beta.Assistant> => {
-  const appSiteUrl = configManager.getConfig('crowi', 'app:siteUrl');
+  const appSiteUrl = configManager.getConfig('app:siteUrl');
   const assistantName = `GROWI ${type} Assistant for ${appSiteUrl}${nameSuffix != null ? ` ${nameSuffix}` : ''}`;
   const assistantName = `GROWI ${type} Assistant for ${appSiteUrl}${nameSuffix != null ? ` ${nameSuffix}` : ''}`;
   const assistantModel = getAssistantModelByType(type);
   const assistantModel = getAssistantModelByType(type);
 
 
@@ -57,7 +68,7 @@ const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Pr
       }));
       }));
 
 
   // update instructions
   // update instructions
-  const instructions = configManager.getConfig('crowi', 'openai:chatAssistantInstructions');
+  const instructions = configManager.getConfig('openai:chatAssistantInstructions');
   openaiClient.beta.assistants.update(assistant.id, {
   openaiClient.beta.assistants.update(assistant.id, {
     instructions,
     instructions,
     model: assistantModel,
     model: assistantModel,
@@ -75,7 +86,7 @@ const getOrCreateAssistant = async(type: AssistantType, nameSuffix?: string): Pr
 
 
 //   searchAssistant = await getOrCreateAssistant(AssistantType.SEARCH);
 //   searchAssistant = await getOrCreateAssistant(AssistantType.SEARCH);
 //   openaiClient.beta.assistants.update(searchAssistant.id, {
 //   openaiClient.beta.assistants.update(searchAssistant.id, {
-//     instructions: configManager.getConfig('crowi', 'openai:searchAssistantInstructions'),
+//     instructions: configManager.getConfig('openai:searchAssistantInstructions'),
 //     tools: [{ type: 'file_search' }],
 //     tools: [{ type: 'file_search' }],
 //   });
 //   });
 
 

+ 1 - 1
apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts

@@ -13,7 +13,7 @@ export class OpenaiClientDelegator implements IOpenaiClientDelegator {
 
 
   constructor() {
   constructor() {
     // Retrieve OpenAI related values from environment variables
     // Retrieve OpenAI related values from environment variables
-    const apiKey = configManager.getConfig('crowi', 'openai:apiKey');
+    const apiKey = configManager.getConfig('openai:apiKey');
 
 
     const isValid = [apiKey].every(value => value != null);
     const isValid = [apiKey].every(value => value != null);
     if (!isValid) {
     if (!isValid) {

+ 1 - 1
apps/app/src/features/openai/server/services/client.ts

@@ -3,5 +3,5 @@ import OpenAI from 'openai';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 
 
 export const openaiClient = new OpenAI({
 export const openaiClient = new OpenAI({
-  apiKey: configManager?.getConfig('crowi', 'openai:apiKey'), // This is the default and can be omitted
+  apiKey: configManager.getConfig('openai:apiKey'), // This is the default and can be omitted
 });
 });

+ 4 - 4
apps/app/src/features/openai/server/services/cron/thread-deletion-cron.ts

@@ -37,10 +37,10 @@ export class ThreadDeletionCronService {
     }
     }
 
 
     this.openaiService = openaiService;
     this.openaiService = openaiService;
-    this.threadDeletionCronExpression = configManager.getConfig('crowi', 'openai:threadDeletionCronExpression');
-    this.threadDeletionCronMaxMinutesUntilRequest = configManager.getConfig('crowi', 'app:openaiThreadDeletionCronMaxMinutesUntilRequest');
-    this.threadDeletionBarchSize = configManager.getConfig('crowi', 'openai:threadDeletionBarchSize');
-    this.threadDeletionApiCallInterval = configManager.getConfig('crowi', 'openai:threadDeletionApiCallInterval');
+    this.threadDeletionCronExpression = configManager.getConfig('openai:threadDeletionCronExpression');
+    this.threadDeletionCronMaxMinutesUntilRequest = configManager.getConfig('app:openaiThreadDeletionCronMaxMinutesUntilRequest');
+    this.threadDeletionBarchSize = configManager.getConfig('openai:threadDeletionBarchSize');
+    this.threadDeletionApiCallInterval = configManager.getConfig('openai:threadDeletionApiCallInterval');
 
 
     this.cronJob?.stop();
     this.cronJob?.stop();
     this.cronJob = this.generateCronJob();
     this.cronJob = this.generateCronJob();

+ 4 - 4
apps/app/src/features/openai/server/services/cron/vector-store-file-deletion-cron.ts

@@ -36,10 +36,10 @@ export class VectorStoreFileDeletionCronService {
     }
     }
 
 
     this.openaiService = openaiService;
     this.openaiService = openaiService;
-    this.vectorStoreFileDeletionCronExpression = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionCronExpression');
-    this.vectorStoreFileDeletionCronMaxMinutesUntilRequest = configManager.getConfig('crowi', 'app:openaiVectorStoreFileDeletionCronMaxMinutesUntilRequest');
-    this.vectorStoreFileDeletionBarchSize = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionBarchSize');
-    this.vectorStoreFileDeletionApiCallInterval = configManager.getConfig('crowi', 'openai:vectorStoreFileDeletionApiCallInterval');
+    this.vectorStoreFileDeletionCronExpression = configManager.getConfig('openai:vectorStoreFileDeletionCronExpression');
+    this.vectorStoreFileDeletionCronMaxMinutesUntilRequest = configManager.getConfig('app:openaiVectorStoreFileDeletionCronMaxMinutesUntilRequest');
+    this.vectorStoreFileDeletionBarchSize = configManager.getConfig('openai:vectorStoreFileDeletionBarchSize');
+    this.vectorStoreFileDeletionApiCallInterval = configManager.getConfig('openai:vectorStoreFileDeletionApiCallInterval');
 
 
     this.cronJob?.stop();
     this.cronJob?.stop();
     this.cronJob = this.generateCronJob();
     this.cronJob = this.generateCronJob();

+ 1 - 1
apps/app/src/features/openai/server/services/is-ai-enabled.ts

@@ -1,3 +1,3 @@
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 
 
-export const isAiEnabled = (): boolean => configManager.getConfig('crowi', 'app:aiEnabled');
+export const isAiEnabled = (): boolean => configManager.getConfig('app:aiEnabled');

+ 3 - 3
apps/app/src/features/openai/server/services/openai.ts

@@ -50,7 +50,7 @@ export interface IOpenaiService {
 class OpenaiService implements IOpenaiService {
 class OpenaiService implements IOpenaiService {
 
 
   private get client() {
   private get client() {
-    const openaiServiceType = configManager.getConfig('crowi', 'openai:serviceType');
+    const openaiServiceType = configManager.getConfig('openai:serviceType');
     return getClient({ openaiServiceType });
     return getClient({ openaiServiceType });
   }
   }
 
 
@@ -364,8 +364,8 @@ export const getOpenaiService = (): IOpenaiService | undefined => {
     return instance;
     return instance;
   }
   }
 
 
-  const aiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
-  const openaiServiceType = configManager.getConfig('crowi', 'openai:serviceType');
+  const aiEnabled = configManager.getConfig('app:aiEnabled');
+  const openaiServiceType = configManager.getConfig('openai:serviceType');
   if (aiEnabled && openaiServiceType != null && OpenaiServiceTypes.includes(openaiServiceType)) {
   if (aiEnabled && openaiServiceType != null && OpenaiServiceTypes.includes(openaiServiceType)) {
     instance = new OpenaiService();
     instance = new OpenaiService();
     return instance;
     return instance;

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

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

+ 76 - 0
apps/app/src/features/opentelemetry/server/logger.ts

@@ -0,0 +1,76 @@
+import { diag, type DiagLogger } from '@opentelemetry/api';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:diag');
+
+
+class DiagLoggerBunyanAdapter implements DiagLogger {
+
+  private parseMessage(message: string, args: unknown[]): [logMessage: string, data: object] {
+    let logMessage = message;
+    let data = {};
+
+    // check whether the message is a JSON string
+    try {
+      const parsedMessage = JSON.parse(message);
+      if (typeof parsedMessage === 'object' && parsedMessage !== null) {
+        data = parsedMessage;
+        // if parsed successfully, use 'message' property as log message
+        logMessage = 'message' in data && typeof data.message === 'string'
+          ? data.message
+          : message;
+      }
+    }
+    catch (e) {
+      // do nothing if the message is not a JSON string
+    }
+
+    // merge additional data
+    if (args.length > 0) {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const argsData = (args as any).reduce((acc, arg) => {
+        if (typeof arg === 'string') {
+          try {
+            const parsed = JSON.parse(arg);
+            return { ...acc, ...parsed };
+          }
+          catch (e) {
+            return { ...acc, additionalInfo: arg };
+          }
+        }
+        return { ...acc, ...arg };
+      }, {});
+      data = { ...data, ...argsData };
+    }
+
+    return [logMessage, data];
+  }
+
+  error(message: string, ...args): void {
+    logger.error(...this.parseMessage(message, args));
+  }
+
+  warn(message: string, ...args): void {
+    logger.warn(...this.parseMessage(message, args));
+  }
+
+  info(message: string, ...args): void {
+    logger.info(...this.parseMessage(message, args));
+  }
+
+  debug(message: string, ...args): void {
+    logger.debug(...this.parseMessage(message, args));
+  }
+
+  verbose(message: string, ...args): void {
+    logger.trace(...this.parseMessage(message, args));
+  }
+
+}
+
+
+export const initLogger = (): void => {
+  // Enable global logger for OpenTelemetry
+  diag.setLogger(new DiagLoggerBunyanAdapter());
+};

+ 67 - 0
apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts

@@ -0,0 +1,67 @@
+import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
+import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
+import { Resource, type IResource } from '@opentelemetry/resources';
+import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
+import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
+import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_INSTANCE_ID } from '@opentelemetry/semantic-conventions';
+
+import { getGrowiVersion } from '~/utils/growi-version';
+
+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_VERSION]: version,
+    });
+
+    configuration = {
+      resource,
+      traceExporter: new OTLPTraceExporter(),
+      metricReader: new PeriodicExportingMetricReader({
+        exporter: new OTLPMetricExporter(),
+      }),
+      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> {
+//   await this.sdkInstance.shutdown();
+
+//   // メモ: 以下の restart コードは動かない
+//   // span/metrics ともに何も出なくなる
+//   // そもそも、restart するような使い方が出来なさそう?
+//   // see: https://github.com/open-telemetry/opentelemetry-specification/issues/27/
+//   // const sdk = new NodeSDK({...});
+//   // sdk.start();
+//   // await sdk.shutdown().catch(console.error);
+//   // const newSdk = new NodeSDK({...});
+//   // newSdk.start();
+// }

+ 103 - 0
apps/app/src/features/opentelemetry/server/node-sdk.ts

@@ -0,0 +1,103 @@
+import { ConfigSource } from '@growi/core/dist/interfaces';
+import type { NodeSDK } from '@opentelemetry/sdk-node';
+
+import { configManager } from '~/server/service/config-manager';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:opentelemetry:server');
+
+
+let sdkInstance: NodeSDK;
+
+/**
+ * Overwrite "OTEL_SDK_DISABLED" env var before sdk.start() is invoked if needed.
+ * Since otel library sees it.
+ */
+function overwriteSdkDisabled(): void {
+  const instrumentationEnabled = configManager.getConfig('otel:enabled', ConfigSource.env);
+
+  if (instrumentationEnabled && (
+    process.env.OTEL_SDK_DISABLED === 'true'
+    || process.env.OTEL_SDK_DISABLED === '1'
+  )) {
+    logger.warn("OTEL_SDK_DISABLED overwritten with 'false' since GROWI's 'otel:enabled' config is true.");
+    process.env.OTEL_SDK_DISABLED = 'false';
+    return;
+  }
+
+  if (!instrumentationEnabled && (
+    process.env.OTEL_SDK_DISABLED === 'false'
+    || process.env.OTEL_SDK_DISABLED === '0'
+  )) {
+    logger.warn("OTEL_SDK_DISABLED is overwritten with 'true' since GROWI's 'otel:enabled' config is false.");
+    process.env.OTEL_SDK_DISABLED = 'true';
+    return;
+  }
+
+}
+
+export const startInstrumentation = async(): Promise<void> => {
+  if (sdkInstance != null) {
+    logger.warn('OpenTelemetry instrumentation already started');
+    return;
+  }
+
+  // load configs from env
+  await configManager.loadConfigs({ source: ConfigSource.env });
+
+  overwriteSdkDisabled();
+
+  const instrumentationEnabled = configManager.getConfig('otel:enabled', ConfigSource.env);
+  if (instrumentationEnabled) {
+
+    logger.info(`GROWI now collects anonymous telemetry.
+
+This data is used to help improve GROWI, but you can opt-out at any time.
+
+For more information, see https://docs.growi.org/en/admin-guide/telemetry.html.
+`);
+
+    // initialize global logger for development
+    const isDev = process.env.NODE_ENV === 'development';
+    if (isDev) {
+      const { initLogger } = await import('./logger');
+      initLogger();
+    }
+
+    // instanciate NodeSDK
+    const { NodeSDK } = await import('@opentelemetry/sdk-node');
+    const { generateNodeSDKConfiguration } = await import('./node-sdk-configuration');
+
+    sdkInstance = new NodeSDK(generateNodeSDKConfiguration());
+    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> {
+//   await this.sdkInstance.shutdown();
+
+//   // メモ: 以下の restart コードは動かない
+//   // span/metrics ともに何も出なくなる
+//   // そもそも、restart するような使い方が出来なさそう?
+//   // see: https://github.com/open-telemetry/opentelemetry-specification/issues/27/
+//   // const sdk = new NodeSDK({...});
+//   // sdk.start();
+//   // await sdk.shutdown().catch(console.error);
+//   // const newSdk = new NodeSDK({...});
+//   // newSdk.start();
+// }

+ 2 - 2
apps/app/src/features/questionnaire/interfaces/condition.ts

@@ -1,7 +1,7 @@
 import type { HasObjectId } from '@growi/core';
 import type { HasObjectId } from '@growi/core';
+import type { GrowiServiceType } from '@growi/core/dist/consts';
 
 
-import { GrowiServiceType } from './growi-info';
-import { UserType } from './user-info';
+import type { UserType } from './user-info';
 
 
 
 
 interface UserCondition {
 interface UserCondition {

+ 18 - 0
apps/app/src/features/questionnaire/interfaces/growi-app-info.ts

@@ -0,0 +1,18 @@
+import type { IGrowiAdditionalInfo, IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { AttachmentMethodType } from '~/interfaces/attachment';
+import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
+
+
+export type IGrowiAppAdditionalInfo = IGrowiAdditionalInfo & {
+  attachmentType: AttachmentMethodType
+  activeExternalAccountTypes?: IExternalAuthProviderType[]
+}
+
+// legacy properties (extracted from additionalInfo for growi-questionnaire)
+// see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
+export type IGrowiAppInfoLegacy = Omit<IGrowiInfo<IGrowiAppAdditionalInfo>, 'additionalInfo'>
+  & IGrowiAppAdditionalInfo
+  & {
+    appSiteUrlHashed: string,
+  };

+ 0 - 58
apps/app/src/features/questionnaire/interfaces/growi-info.ts

@@ -1,58 +0,0 @@
-import * as os from 'node:os';
-
-import { IExternalAuthProviderType } from '@growi/core';
-
-export const GrowiServiceType = {
-  cloud: 'cloud',
-  privateCloud: 'private-cloud',
-  onPremise: 'on-premise',
-  others: 'others',
-} as const;
-export const GrowiWikiType = { open: 'open', closed: 'closed' } as const;
-export const GrowiAttachmentType = {
-  aws: 'aws',
-  gcs: 'gcs',
-  gcp: 'gcp',
-  azure: 'azure',
-  gridfs: 'gridfs',
-  mongo: 'mongo',
-  mongodb: 'mongodb',
-  local: 'local',
-  none: 'none',
-} as const;
-export const GrowiDeploymentType = {
-  officialHelmChart: 'official-helm-chart',
-  growiDockerCompose: 'growi-docker-compose',
-  node: 'node',
-  others: 'others',
-} as const;
-export const GrowiExternalAuthProviderType = IExternalAuthProviderType;
-
-export type GrowiServiceType = typeof GrowiServiceType[keyof typeof GrowiServiceType]
-type GrowiWikiType = typeof GrowiWikiType[keyof typeof GrowiWikiType]
-export type GrowiAttachmentType = typeof GrowiAttachmentType[keyof typeof GrowiAttachmentType]
-export type GrowiDeploymentType = typeof GrowiDeploymentType[keyof typeof GrowiDeploymentType]
-export type GrowiExternalAuthProviderType = typeof GrowiExternalAuthProviderType[keyof typeof GrowiExternalAuthProviderType]
-
-interface IGrowiOSInfo {
-  type?: ReturnType<typeof os.type>
-  platform?: ReturnType<typeof os.platform>
-  arch?: ReturnType<typeof os.arch>
-  totalmem?: ReturnType<typeof os.totalmem>
-}
-
-export interface IGrowiInfo {
-  version: string
-  appSiteUrl?: string
-  appSiteUrlHashed: string
-  installedAt: Date
-  installedAtByOldestUser: Date
-  type: GrowiServiceType
-  currentUsersCount: number
-  currentActiveUsersCount: number
-  wikiType: GrowiWikiType
-  attachmentType: GrowiAttachmentType
-  activeExternalAccountTypes?: GrowiExternalAuthProviderType[]
-  osInfo?: IGrowiOSInfo
-  deploymentType?: GrowiDeploymentType
-}

+ 16 - 3
apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts

@@ -1,11 +1,24 @@
-import { IGrowiInfo } from './growi-info';
-import { IUserInfo } from './user-info';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
+import type { IUserInfo } from './user-info';
 
 
 
 
 export interface IProactiveQuestionnaireAnswer {
 export interface IProactiveQuestionnaireAnswer {
   satisfaction: number,
   satisfaction: number,
   commentText: string,
   commentText: string,
-  growiInfo: IGrowiInfo,
+  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>,
+  userInfo: IUserInfo,
+  answeredAt: Date,
+  lengthOfExperience?: string,
+  position?: string,
+  occupation?: string,
+}
+
+export interface IProactiveQuestionnaireAnswerLegacy {
+  satisfaction: number,
+  commentText: string,
+  growiInfo: IGrowiAppInfoLegacy,
   userInfo: IUserInfo,
   userInfo: IUserInfo,
   answeredAt: Date,
   answeredAt: Date,
   lengthOfExperience?: string,
   lengthOfExperience?: string,

+ 14 - 4
apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts

@@ -1,11 +1,21 @@
-import { IAnswer } from './answer';
-import { IGrowiInfo } from './growi-info';
-import { IUserInfo } from './user-info';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { IAnswer } from './answer';
+import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
+import type { IUserInfo } from './user-info';
 
 
 export interface IQuestionnaireAnswer<ID = string> {
 export interface IQuestionnaireAnswer<ID = string> {
   answers: IAnswer[]
   answers: IAnswer[]
   answeredAt: Date
   answeredAt: Date
-  growiInfo: IGrowiInfo
+  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>
+  userInfo: IUserInfo
+  questionnaireOrder: ID
+}
+
+export interface IQuestionnaireAnswerLegacy<ID = string> {
+  answers: IAnswer[]
+  answeredAt: Date
+  growiInfo: IGrowiAppInfoLegacy,
   userInfo: IUserInfo
   userInfo: IUserInfo
   questionnaireOrder: ID
   questionnaireOrder: ID
 }
 }

+ 3 - 2
apps/app/src/features/questionnaire/server/models/questionnaire-order.ts

@@ -1,8 +1,9 @@
-import { Model, Schema, Document } from 'mongoose';
+import type { Model, Document } from 'mongoose';
+import { Schema } from 'mongoose';
 
 
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
 
-import { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
+import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
 
 
 import conditionSchema from './schema/condition';
 import conditionSchema from './schema/condition';
 import questionSchema from './schema/question';
 import questionSchema from './schema/question';

+ 2 - 2
apps/app/src/features/questionnaire/server/models/schema/condition.ts

@@ -1,7 +1,7 @@
+import { GrowiServiceType } from '@growi/core/dist/consts';
 import { Schema } from 'mongoose';
 import { Schema } from 'mongoose';
 
 
-import { ICondition } from '../../../interfaces/condition';
-import { GrowiServiceType } from '../../../interfaces/growi-info';
+import type { ICondition } from '../../../interfaces/condition';
 import { UserType } from '../../../interfaces/user-info';
 import { UserType } from '../../../interfaces/user-info';
 
 
 const conditionSchema = new Schema<ICondition>({
 const conditionSchema = new Schema<ICondition>({

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

@@ -1,21 +1,27 @@
+import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+import { GrowiWikiType } from '@growi/core/dist/interfaces';
 import { Schema } from 'mongoose';
 import { Schema } from 'mongoose';
 
 
-import {
-  GrowiAttachmentType, GrowiDeploymentType, GrowiExternalAuthProviderType, GrowiServiceType, GrowiWikiType, IGrowiInfo,
-} from '../../../interfaces/growi-info';
+import type { IGrowiAppAdditionalInfo } from '~/features/questionnaire/interfaces/growi-app-info';
+import { AttachmentMethodType } from '~/interfaces/attachment';
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 
 
-export const growiInfoSchema = new Schema<IGrowiInfo>({
-  version: { type: String, required: true },
-  appSiteUrl: { type: String },
-  appSiteUrlHashed: { type: String, required: true },
+const growiAdditionalInfoSchema = new Schema<IGrowiAppAdditionalInfo>({
   installedAt: { type: Date, required: true },
   installedAt: { type: Date, required: true },
   installedAtByOldestUser: { type: Date, required: true },
   installedAtByOldestUser: { type: Date, required: true },
-  type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
   currentUsersCount: { type: Number, required: true },
   currentUsersCount: { type: Number, required: true },
   currentActiveUsersCount: { type: Number, required: true },
   currentActiveUsersCount: { type: Number, required: true },
+  attachmentType: { type: String, required: true, enum: Object.values(AttachmentMethodType) },
+  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
+});
+
+export const growiInfoSchema = new Schema<IGrowiInfo<IGrowiAppAdditionalInfo> & IGrowiAppAdditionalInfo>({
+  version: { type: String, required: true },
+  appSiteUrl: { type: String },
+  serviceInstanceId: { type: String, required: true },
+  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) },
-  attachmentType: { type: String, required: true, enum: Object.values(GrowiAttachmentType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(GrowiExternalAuthProviderType) }],
   osInfo: {
   osInfo: {
     type: { type: String },
     type: { type: String },
     platform: String,
     platform: String,
@@ -23,4 +29,14 @@ export const growiInfoSchema = new Schema<IGrowiInfo>({
     totalmem: Number,
     totalmem: Number,
   },
   },
   deploymentType: { type: String, enum: (<(string | null)[]>Object.values(GrowiDeploymentType)).concat([null]) },
   deploymentType: { type: String, enum: (<(string | null)[]>Object.values(GrowiDeploymentType)).concat([null]) },
+  additionalInfo: growiAdditionalInfoSchema,
+
+  // legacy properties (extracted from additionalInfo for growi-questionnaire)
+  // see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
+  installedAt: { type: Date },
+  installedAtByOldestUser: { type: Date },
+  currentUsersCount: { type: Number },
+  currentActiveUsersCount: { type: Number },
+  attachmentType: { type: String, enum: Object.values(AttachmentMethodType) },
+  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
 });
 });

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

@@ -6,6 +6,8 @@ import { body, validationResult } from 'express-validator';
 import type Crowi from '~/server/crowi';
 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 { 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';
 
 
@@ -16,6 +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, getSiteUrlHashed } from '../../util/convert-to-legacy-format';
 
 
 
 
 const logger = loggerFactory('growi:routes:apiv3:questionnaire');
 const logger = loggerFactory('growi:routes:apiv3:questionnaire');
@@ -59,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);
@@ -74,15 +77,16 @@ module.exports = (crowi: Crowi): Router => {
   });
   });
 
 
   router.get('/is-enabled', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
   router.get('/is-enabled', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const isEnabled = crowi.configManager!.getConfig('crowi', 'questionnaire:isQuestionnaireEnabled');
+    const isEnabled = configManager.getConfig('questionnaire:isQuestionnaireEnabled');
     return res.apiv3({ isEnabled });
     return res.apiv3({ isEnabled });
   });
   });
 
 
   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 = crowi.configManager?.getConfig('crowi', 'app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-      const userInfo = crowi.questionnaireService!.getUserInfo(req.user ?? null, growiInfo.appSiteUrlHashed);
+      const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
+      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,
@@ -95,8 +99,10 @@ module.exports = (crowi: Crowi): Router => {
         answeredAt: new Date(),
         answeredAt: new Date(),
       };
       };
 
 
+      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer, isAppSiteUrlHashed);
+
       try {
       try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswer);
+        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
       }
       }
       catch (err) {
       catch (err) {
         if (err.request != null) {
         if (err.request != null) {
@@ -126,9 +132,10 @@ 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('crowi', 'app:questionnaireServerOrigin');
-      const growiInfo = await crowi.questionnaireService!.getGrowiInfo();
-      const userInfo = crowi.questionnaireService!.getUserInfo(user, growiInfo.appSiteUrlHashed);
+      const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
+      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,
@@ -138,8 +145,10 @@ module.exports = (crowi: Crowi): Router => {
         questionnaireOrder: req.body.questionnaireOrderId,
         questionnaireOrder: req.body.questionnaireOrderId,
       };
       };
 
 
+      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer, isAppSiteUrlHashed);
+
       try {
       try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswer);
+        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswerLegacy);
       }
       }
       catch (err) {
       catch (err) {
         if (err.request != null) {
         if (err.request != null) {

+ 22 - 11
apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts

@@ -1,14 +1,17 @@
 import axiosRetry from 'axios-retry';
 import axiosRetry from 'axios-retry';
 
 
+import type Crowi from '~/server/crowi';
+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';
 
 
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
+import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
 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 QuestionnaireOrder from '../models/questionnaire-order';
 import QuestionnaireOrder from '../models/questionnaire-order';
+import { convertToLegacyFormat } from '../util/convert-to-legacy-format';
 
 
 const logger = loggerFactory('growi:service:questionnaire-cron');
 const logger = loggerFactory('growi:service:questionnaire-cron');
 
 
@@ -26,20 +29,19 @@ axiosRetry(axios, { retries: 3 });
  */
  */
 class QuestionnaireCronService {
 class QuestionnaireCronService {
 
 
-  crowi: any;
+  crowi: Crowi;
 
 
   cronJob: any;
   cronJob: any;
 
 
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
   sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
   sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
 
 
   startCron(): void {
   startCron(): void {
-    const cronSchedule = this.crowi.configManager?.getConfig('crowi', 'app:questionnaireCronSchedule');
-    const maxHoursUntilRequest = this.crowi.configManager?.getConfig('crowi', 'app:questionnaireCronMaxHoursUntilRequest');
+    const cronSchedule = this.crowi.configManager.getConfig('app:questionnaireCronSchedule');
+    const maxHoursUntilRequest = this.crowi.configManager.getConfig('app:questionnaireCronMaxHoursUntilRequest');
 
 
     const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
     const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
 
 
@@ -53,7 +55,8 @@ class QuestionnaireCronService {
   }
   }
 
 
   async executeJob(): Promise<void> {
   async executeJob(): Promise<void> {
-    const questionnaireServerOrigin = this.crowi.configManager?.getConfig('crowi', '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`);
@@ -75,15 +78,23 @@ class QuestionnaireCronService {
 
 
     const resendQuestionnaireAnswers = async() => {
     const resendQuestionnaireAnswers = async() => {
       const questionnaireAnswers = await QuestionnaireAnswer.find()
       const questionnaireAnswers = await QuestionnaireAnswer.find()
-        .select('-_id -answers._id  -growiInfo._id -userInfo._id');
+        .select('-_id -answers._id  -growiInfo._id -userInfo._id')
+        .lean();
       const proactiveQuestionnaireAnswers = await ProactiveQuestionnaireAnswer.find()
       const proactiveQuestionnaireAnswers = await ProactiveQuestionnaireAnswer.find()
-        .select('-_id -growiInfo._id -userInfo._id');
+        .select('-_id -growiInfo._id -userInfo._id')
+        .lean();
 
 
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, { questionnaireAnswers })
+      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
+        // convert to legacy format
+        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
+      })
         .then(async() => {
         .then(async() => {
           await QuestionnaireAnswer.deleteMany();
           await QuestionnaireAnswer.deleteMany();
         });
         });
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, { proactiveQuestionnaireAnswers })
+      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
+        // convert to legacy format
+        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
+      })
         .then(async() => {
         .then(async() => {
           await ProactiveQuestionnaireAnswer.deleteMany();
           await ProactiveQuestionnaireAnswer.deleteMany();
         });
         });

+ 301 - 0
apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts

@@ -0,0 +1,301 @@
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+import { mock } from 'vitest-mock-extended';
+
+import pkg from '^/package.json';
+
+
+import type UserEvent from '~/server/events/user';
+import { configManager } from '~/server/service/config-manager';
+
+import type Crowi from '../../../../server/crowi';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
+import { StatusType } from '../../interfaces/questionnaire-answer-status';
+import { UserType } from '../../interfaces/user-info';
+import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
+import QuestionnaireOrder from '../models/questionnaire-order';
+
+import QuestionnaireService from './questionnaire';
+
+
+describe('QuestionnaireService', () => {
+  const appVersion = pkg.version;
+
+  let questionnaireService: QuestionnaireService;
+
+  let User;
+  let user;
+
+  beforeAll(async() => {
+
+    await configManager.loadConfigs();
+
+    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
+    user = await User.create({
+      name: 'Example for Questionnaire Service Test',
+      username: 'questionnaire test user',
+      email: 'questionnaireTestUser@example.com',
+      password: 'usertestpass',
+      createdAt: '2000-01-01',
+    });
+
+    questionnaireService = new QuestionnaireService(crowiMock);
+  });
+
+  describe('getUserInfo', () => {
+    test('Should get correct user info when user given', () => {
+      const userInfo = questionnaireService.getUserInfo(user, 'growiurlhashfortest');
+      expect(userInfo).not.toBeNull();
+      assert(userInfo != null);
+
+      expect(userInfo.type).equal(UserType.general);
+      assert(userInfo.type === UserType.general);
+
+      expect(userInfo.userIdHash).toBeTruthy();
+      expect(userInfo.userIdHash).not.toBe(user._id);
+
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      delete (userInfo as any).userIdHash;
+
+      expect(userInfo).toEqual({ type: 'general', userCreatedAt: new Date('2000-01-01') });
+    });
+
+    test('Should get correct user info when user is null', () => {
+      const userInfo = questionnaireService.getUserInfo(null, '');
+      expect(userInfo).toEqual({ type: 'guest' });
+    });
+  });
+
+  describe('getQuestionnaireOrdersToShow', () => {
+    let doc1;
+    let doc2;
+    let doc3;
+    let doc4;
+    let doc5;
+    let doc6;
+    let doc7;
+    let doc8;
+    let doc9;
+    let doc10;
+    let doc11;
+    let doc12;
+
+    beforeAll(async() => {
+      const questionnaireToBeShown = {
+        shortTitle: {
+          ja_JP: 'GROWI に関するアンケート',
+          en_US: 'Questions about GROWI',
+        },
+        title: {
+          ja_JP: 'GROWI に関するアンケート',
+          en_US: 'Questions about GROWI',
+        },
+        showFrom: '2022-12-11',
+        showUntil: '2100-12-12',
+        condition: {
+          user: {
+            types: ['general'],
+            daysSinceCreation: {
+              moreThanOrEqualTo: 365,
+              lessThanOrEqualTo: 365 * 1000,
+            },
+          },
+          growi: {
+            types: ['on-premise'],
+            versionRegExps: [appVersion],
+          },
+        },
+        createdAt: '2023-01-01',
+        updatedAt: '2023-01-01',
+      };
+
+      // insert initial db data
+      doc1 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert finished data
+      doc2 = await QuestionnaireOrder.create({
+        ...questionnaireToBeShown,
+        showFrom: '2020-12-11',
+        showUntil: '2021-12-12',
+      });
+      // insert data for admin or guest
+      doc3 = await QuestionnaireOrder.create({
+        ...questionnaireToBeShown,
+        condition: {
+          user: {
+            types: ['admin', 'guest'],
+          },
+          growi: {
+            types: ['on-premise'],
+            versionRegExps: [appVersion],
+          },
+        },
+      });
+      // insert answered data
+      doc4 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert skipped data
+      doc5 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert denied data
+      doc6 = await QuestionnaireOrder.create(questionnaireToBeShown);
+      // insert data for different growi type
+      doc7 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+            },
+            growi: {
+              types: ['cloud'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+      // insert data for different growi version
+      doc8 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: ['1.0.0-alpha'],
+            },
+          },
+        },
+      );
+      // insert data for users that used GROWI for less than or equal to a year
+      doc9 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                lessThanOrEqualTo: 365,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+      // insert data for users that used GROWI for more than or equal to 1000 years
+      doc10 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                moreThanOrEqualTo: 365 * 1000,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+      // insert data for users that used GROWI for more than a month and less than 6 months
+      doc11 = await QuestionnaireOrder.create(
+        {
+          ...questionnaireToBeShown,
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                moreThanOrEqualTo: 30,
+                lessThanOrEqualTo: 30 * 6,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+        },
+      );
+
+      await QuestionnaireAnswerStatus.insertMany([
+        {
+          user: user._id,
+          questionnaireOrderId: doc4._id,
+          status: StatusType.answered,
+        },
+        {
+          user: user._id,
+          questionnaireOrderId: doc5._id,
+          status: StatusType.skipped,
+        },
+        {
+          user: user._id,
+          questionnaireOrderId: doc6._id,
+          status: StatusType.skipped,
+        },
+      ]);
+    });
+
+    test('Should get questionnaire orders to show', async() => {
+      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);
+
+      expect(questionnaireOrderDocuments[0].toObject()).toMatchObject(
+        {
+          __v: 0,
+          shortTitle: {
+            ja_JP: 'GROWI に関するアンケート',
+            en_US: 'Questions about GROWI',
+          },
+          title: {
+            ja_JP: 'GROWI に関するアンケート',
+            en_US: 'Questions about GROWI',
+          },
+          showFrom: new Date('2022-12-11'),
+          showUntil: new Date('2100-12-12'),
+          questions: [],
+          condition: {
+            user: {
+              types: ['general'],
+              daysSinceCreation: {
+                moreThanOrEqualTo: 365,
+                lessThanOrEqualTo: 365 * 1000,
+              },
+            },
+            growi: {
+              types: ['on-premise'],
+              versionRegExps: [appVersion],
+            },
+          },
+          createdAt: new Date('2023-01-01'),
+          updatedAt: new Date('2023-01-01'),
+        },
+      );
+
+    });
+
+  });
+
+});

+ 8 - 70
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -1,18 +1,13 @@
 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 } from '@growi/core/dist/interfaces';
 
 
+import type Crowi from '~/server/crowi';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-// eslint-disable-next-line import/no-named-as-default
-import { Config } from '~/server/models/config';
-import { aclService } from '~/server/service/acl';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import type { IGrowiInfo } from '../../interfaces/growi-info';
-import {
-  GrowiWikiType, GrowiExternalAuthProviderType, GrowiServiceType, GrowiAttachmentType, GrowiDeploymentType,
-} from '../../interfaces/growi-info';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { type IUserInfo, UserType } from '../../interfaces/user-info';
 import { type IUserInfo, UserType } from '../../interfaces/user-info';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
@@ -25,72 +20,13 @@ const logger = loggerFactory('growi:service:questionnaire');
 
 
 class QuestionnaireService {
 class QuestionnaireService {
 
 
-  crowi: any;
+  crowi: Crowi;
 
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
-  async getGrowiInfo(): Promise<IGrowiInfo> {
-    const User = this.crowi.model('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 installedAt = appInstalledConfig != null && appInstalledConfig.createdAt != null ? appInstalledConfig.createdAt : installedAtByOldestUser;
-
-    const currentUsersCount = await User.countDocuments();
-    const currentActiveUsersCount = await User.countActiveUsers();
-
-    const isGuestAllowedToRead = aclService.isGuestAllowedToRead();
-    const wikiType = isGuestAllowedToRead ? GrowiWikiType.open : GrowiWikiType.closed;
-
-    const activeExternalAccountTypes: GrowiExternalAuthProviderType[] = Object.values(GrowiExternalAuthProviderType).filter((type) => {
-      return this.crowi.configManager.getConfig('crowi', `security:passport-${type}:isEnabled`);
-    });
-
-    const typeStr = this.crowi.configManager.getConfig('crowi', 'app:serviceType');
-    const type = Object.values(GrowiServiceType).includes(typeStr) ? typeStr : null;
-
-    const attachmentTypeStr = this.crowi.configManager.getConfig('crowi', 'app:fileUploadType');
-    const attachmentType = Object.values(GrowiAttachmentType).includes(attachmentTypeStr) ? attachmentTypeStr : null;
-
-    const deploymentTypeStr = this.crowi.configManager.getConfig('crowi', 'app:deploymentType');
-    const deploymentType = Object.values(GrowiDeploymentType).includes(deploymentTypeStr) ? deploymentTypeStr : null;
-
-    return {
-      version: this.crowi.version,
-      osInfo: {
-        type: os.type(),
-        platform: os.platform(),
-        arch: os.arch(),
-        totalmem: os.totalmem(),
-      },
-      appSiteUrl: this.crowi.configManager.getConfig('crowi', 'questionnaire:isAppSiteUrlHashed') ? null : appSiteUrl,
-      appSiteUrlHashed,
-      installedAt,
-      installedAtByOldestUser,
-      type,
-      currentUsersCount,
-      currentActiveUsersCount,
-      wikiType,
-      attachmentType,
-      activeExternalAccountTypes,
-      deploymentType,
-    };
-  }
-
   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);
@@ -106,7 +42,9 @@ class QuestionnaireService {
     return { type: UserType.guest };
     return { type: UserType.guest };
   }
   }
 
 
-  async getQuestionnaireOrdersToShow(userInfo: IUserInfo, growiInfo: IGrowiInfo, userId: ObjectIdLike | null): Promise<QuestionnaireOrderDocument[]> {
+  async getQuestionnaireOrdersToShow(
+      userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>, userId: ObjectIdLike | null,
+  ): Promise<QuestionnaireOrderDocument[]> {
     const currentDate = new Date();
     const currentDate = new Date();
 
 
     let questionnaireOrders = await QuestionnaireOrder.find({
     let questionnaireOrders = await QuestionnaireOrder.find({

+ 9 - 6
apps/app/src/features/questionnaire/server/util/condition.ts

@@ -1,7 +1,10 @@
-import { ICondition } from '../../interfaces/condition';
-import { IGrowiInfo } from '../../interfaces/growi-info';
-import { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import { IUserInfo, UserType } from '../../interfaces/user-info';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+
+import type { ICondition } from '../../interfaces/condition';
+import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
+import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
+import type { IUserInfo } from '../../interfaces/user-info';
+import { UserType } from '../../interfaces/user-info';
 
 
 
 
 const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
 const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
@@ -39,7 +42,7 @@ const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
   return true;
   return true;
 };
 };
 
 
-const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo): boolean => {
+const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
   const { growi: { types, versionRegExps } } = condition;
   const { growi: { types, versionRegExps } } = condition;
 
 
   if (!types.includes(growiInfo.type)) {
   if (!types.includes(growiInfo.type)) {
@@ -53,7 +56,7 @@ const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo): boolean =
   return true;
   return true;
 };
 };
 
 
-export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo): boolean => {
+export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
   const { condition } = order;
   const { condition } = order;
 
 
   if (!checkUserInfo(condition, userInfo)) {
   if (!checkUserInfo(condition, userInfo)) {

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

@@ -0,0 +1,125 @@
+import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
+import type { IGrowiInfo } from '@growi/core/dist/interfaces';
+import { GrowiWikiType } from '@growi/core/dist/interfaces';
+import {
+  describe, test, expect,
+} from 'vitest';
+import { mock } from 'vitest-mock-extended';
+
+import { AttachmentMethodType } from '../../../../interfaces/attachment';
+import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
+
+import { convertToLegacyFormat } from './convert-to-legacy-format';
+
+describe('convertToLegacyFormat', () => {
+  test('should return same object when input is already in legacy format', () => {
+    const growiInfoLegacy: IGrowiAppInfoLegacy = {
+      version: '1.0.0',
+      appSiteUrl: 'https://example.com',
+      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
+      serviceInstanceId: 'service-instance-id',
+      type: GrowiServiceType.cloud,
+      wikiType: GrowiWikiType.open,
+      deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
+
+      // legacy properties
+      installedAt: new Date(),
+      installedAtByOldestUser: new Date(),
+      currentUsersCount: 1,
+      currentActiveUsersCount: 1,
+      attachmentType: AttachmentMethodType.local,
+    };
+
+    const legacyData = {
+      someData: 'test',
+      growiInfo: growiInfoLegacy,
+    };
+
+    const result = convertToLegacyFormat(legacyData);
+    expect(result).toStrictEqual(legacyData);
+  });
+
+  test('should convert new format to legacy format', () => {
+    const growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo> = {
+      version: '1.0.0',
+      appSiteUrl: 'https://example.com',
+      serviceInstanceId: 'service-instance-id',
+      type: GrowiServiceType.cloud,
+      wikiType: GrowiWikiType.open,
+      deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
+      additionalInfo: {
+        installedAt: new Date(),
+        installedAtByOldestUser: new Date(),
+        currentUsersCount: 1,
+        currentActiveUsersCount: 1,
+        attachmentType: AttachmentMethodType.local,
+      },
+    };
+    const newFormatData = {
+      someData: 'test',
+      growiInfo,
+    };
+
+    const growiInfoLegacy: IGrowiAppInfoLegacy = {
+      version: '1.0.0',
+      appSiteUrl: 'https://example.com',
+      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
+      serviceInstanceId: 'service-instance-id',
+      type: GrowiServiceType.cloud,
+      wikiType: GrowiWikiType.open,
+      deploymentType: GrowiDeploymentType.others,
+      osInfo: {
+        type: 'Linux',
+        platform: 'linux',
+        arch: 'x64',
+        totalmem: 8589934592,
+      },
+
+      // legacy properties
+      installedAt: new Date(),
+      installedAtByOldestUser: new Date(),
+      currentUsersCount: 1,
+      currentActiveUsersCount: 1,
+      attachmentType: AttachmentMethodType.local,
+    };
+    const expected = {
+      someData: 'test',
+      growiInfo: growiInfoLegacy,
+    };
+
+    const result = convertToLegacyFormat(newFormatData);
+    expect(result).toStrictEqual(expected);
+  });
+
+  test('should convert new format and omit appSiteUrl', () => {
+    // arrange
+    const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
+      appSiteUrl: 'https://example.com',
+      additionalInfo: {
+        installedAt: new Date(),
+        installedAtByOldestUser: new Date(),
+        currentUsersCount: 1,
+        currentActiveUsersCount: 1,
+        attachmentType: AttachmentMethodType.local,
+      },
+    });
+
+    // act
+    const result = convertToLegacyFormat({ growiInfo }, true);
+
+    // assert
+    expect(result.growiInfo.appSiteUrl).toBeUndefined();
+  });
+});

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

@@ -0,0 +1,40 @@
+import assert from 'assert';
+import crypto from 'crypto';
+
+import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
+
+
+type IHasGrowiAppInfoLegacy<T> = T & {
+  growiInfo: IGrowiAppInfoLegacy;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function isLegacy<T extends { growiInfo: any }>(data: T): data is IHasGrowiAppInfoLegacy<T> {
+  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
+export function convertToLegacyFormat<T extends { growiInfo: any }>(questionnaireAnswer: T, isAppSiteUrlHashed = false): IHasGrowiAppInfoLegacy<T> {
+  if (isLegacy(questionnaireAnswer)) {
+    return questionnaireAnswer;
+  }
+
+  const { additionalInfo, appSiteUrl, ...rest } = questionnaireAnswer.growiInfo;
+  assert(additionalInfo != null);
+
+  return {
+    ...questionnaireAnswer,
+    growiInfo: {
+      appSiteUrl: isAppSiteUrlHashed ? undefined : appSiteUrl,
+      appSiteUrlHashed: getSiteUrlHashed(appSiteUrl),
+      ...rest,
+      ...additionalInfo,
+    },
+  };
+}

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

@@ -8,6 +8,7 @@ import { param, query } from 'express-validator';
 
 
 import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { PLUGIN_STORING_PATH } from '~/features/growi-plugin/server/consts';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
+import type Crowi from '~/server/crowi';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -32,7 +33,7 @@ const validator = {
 let presetTemplateSummaries: TemplateSummary[];
 let presetTemplateSummaries: TemplateSummary[];
 
 
 
 
-module.exports = (crowi) => {
+module.exports = (crowi: Crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
 
   router.get('/', loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
   router.get('/', loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {

+ 1 - 0
apps/app/src/interfaces/activity.ts

@@ -365,6 +365,7 @@ export const ActionGroupSize = {
   Medium: 'MEDIUM',
   Medium: 'MEDIUM',
   Large: 'LARGE',
   Large: 'LARGE',
 } as const;
 } as const;
+export type ActionGroupSize = typeof ActionGroupSize[keyof typeof ActionGroupSize];
 
 
 export const SmallActionGroup = {
 export const SmallActionGroup = {
   ACTION_USER_LOGIN_WITH_LOCAL,
   ACTION_USER_LOGIN_WITH_LOCAL,

+ 13 - 1
apps/app/src/interfaces/attachment.ts

@@ -1,7 +1,19 @@
-import type { IAttachmentHasId } from '@growi/core';
+import type { IAttachmentHasId } from '@growi/core/dist/interfaces';
 
 
 import type { PaginateResult } from './mongoose-utils';
 import type { PaginateResult } from './mongoose-utils';
 
 
+export const AttachmentMethodType = {
+  aws: 'aws',
+  gcs: 'gcs',
+  gcp: 'gcp',
+  azure: 'azure',
+  gridfs: 'gridfs',
+  mongo: 'mongo',
+  mongodb: 'mongodb',
+  local: 'local',
+  none: 'none',
+} as const;
+export type AttachmentMethodType = typeof AttachmentMethodType[keyof typeof AttachmentMethodType]
 
 
 export type IResAttachmentList = {
 export type IResAttachmentList = {
   paginateResult: PaginateResult<IAttachmentHasId>
   paginateResult: PaginateResult<IAttachmentHasId>

+ 3 - 2
apps/app/src/interfaces/crowi-request.ts

@@ -2,13 +2,14 @@ import type { IUser } from '@growi/core';
 import type { Request } from 'express';
 import type { Request } from 'express';
 import type { HydratedDocument } from 'mongoose';
 import type { HydratedDocument } from 'mongoose';
 
 
+import type Crowi from '~/server/crowi';
+
 
 
 export interface CrowiProperties {
 export interface CrowiProperties {
 
 
   user?: HydratedDocument<IUser>,
   user?: HydratedDocument<IUser>,
 
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  crowi: any,
+  crowi: Crowi,
 
 
   session: any,
   session: any,
 
 

+ 9 - 0
apps/app/src/interfaces/external-auth-provider.ts

@@ -0,0 +1,9 @@
+export const IExternalAuthProviderType = {
+  ldap: 'ldap',
+  saml: 'saml',
+  oidc: 'oidc',
+  google: 'google',
+  github: 'github',
+} as const;
+
+export type IExternalAuthProviderType = typeof IExternalAuthProviderType[keyof typeof IExternalAuthProviderType]

+ 38 - 0
apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js

@@ -0,0 +1,38 @@
+import mongoose from 'mongoose';
+
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:remove-index-for-ns-from-configs');
+
+async function dropIndexIfExists(db, collectionName, indexName) {
+  // check existence of the collection
+  const items = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
+  if (items.length === 0) {
+    return;
+  }
+
+  const collection = await db.collection(collectionName);
+  if (await collection.indexExists(indexName)) {
+    await collection.dropIndex(indexName);
+  }
+}
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    await mongoose.connect(getMongoUri(), mongoOptions);
+
+    // drop index
+    await dropIndexIfExists(db, 'configs', 'ns_1_key_1');
+
+    // create index
+    const collection = await db.collection('configs');
+    await collection.createIndex({ key: 1 }, { unique: true });
+  },
+
+  async down() {
+    // No rollback
+  },
+};

+ 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 - 6
apps/app/src/migrations/20180927102719-init-serverurl.js

@@ -24,7 +24,6 @@ module.exports = {
 
 
     // find 'app:siteUrl'
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
     const siteUrlConfig = await Config.findOne({
-      ns: 'crowi',
       key: 'app:siteUrl',
       key: 'app:siteUrl',
     });
     });
     // exit if exists
     // exit if exists
@@ -35,7 +34,6 @@ module.exports = {
 
 
     // find all callbackUrls
     // find all callbackUrls
     const configs = await Config.find({
     const configs = await Config.find({
-      ns: 'crowi',
       $or: [
       $or: [
         { key: 'security:passport-github:callbackUrl' },
         { key: 'security:passport-github:callbackUrl' },
         { key: 'security:passport-google:callbackUrl' },
         { key: 'security:passport-google:callbackUrl' },
@@ -63,11 +61,10 @@ module.exports = {
     }
     }
 
 
     if (siteUrl != null) {
     if (siteUrl != null) {
-      const ns = 'crowi';
       const key = 'app:siteUrl';
       const key = 'app:siteUrl';
       await Config.findOneAndUpdate(
       await Config.findOneAndUpdate(
-        { ns, key },
-        { ns, key, value: JSON.stringify(siteUrl) },
+        { key },
+        { key, value: JSON.stringify(siteUrl) },
         { upsert: true },
         { upsert: true },
       );
       );
       logger.info('Migration has successfully applied');
       logger.info('Migration has successfully applied');
@@ -80,7 +77,6 @@ module.exports = {
 
 
     // remote 'app:siteUrl'
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({
-      ns: 'crowi',
       key: 'app:siteUrl',
       key: 'app:siteUrl',
     });
     });
 
 

+ 6 - 6
apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -15,18 +15,18 @@ module.exports = {
     // enable passport and delete configs for crowi classic auth
     // enable passport and delete configs for crowi classic auth
     await Promise.all([
     await Promise.all([
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
-        { ns: 'crowi', key: 'security:isEnabledPassport' },
-        { ns: 'crowi', key: 'security:isEnabledPassport', value: JSON.stringify(true) },
+        { key: 'security:isEnabledPassport' },
+        { key: 'security:isEnabledPassport', value: JSON.stringify(true) },
         { upsert: true },
         { upsert: true },
       ),
       ),
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
-        { ns: 'crowi', key: 'google:clientId' },
-        { ns: 'crowi', key: 'google:clientId', value: JSON.stringify(null) },
+        { key: 'google:clientId' },
+        { key: 'google:clientId', value: JSON.stringify(null) },
         { upsert: true },
         { upsert: true },
       ),
       ),
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
-        { ns: 'crowi', key: 'google:clientSecret' },
-        { ns: 'crowi', key: 'google:clientSecret', value: JSON.stringify(null) },
+        { key: 'google:clientSecret' },
+        { key: 'google:clientSecret', value: JSON.stringify(null) },
         { upsert: true },
         { upsert: true },
       ),
       ),
     ]);
     ]);

+ 3 - 6
apps/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -9,9 +9,9 @@ const logger = loggerFactory('growi:migrate:add-config-app-installed');
 
 
 /**
 /**
  * BEFORE
  * BEFORE
- *   - Config document { ns: 'crowi', key: 'app:installed' } does not exist
+ *   - Config document { key: 'app:installed' } does not exist
  * AFTER
  * AFTER
- *   - Config document { ns: 'crowi', key: 'app:installed' } is created
+ *   - Config document { key: 'app:installed' } is created
  *     - value will be true if one or more users exist
  *     - value will be true if one or more users exist
  *     - value will be false if no users exist
  *     - value will be false if no users exist
  */
  */
@@ -23,9 +23,8 @@ module.exports = {
 
 
     const User = userModelFactory();
     const User = userModelFactory();
 
 
-    // find 'app:siteUrl'
+    // find 'app:installed'
     const appInstalled = await Config.findOne({
     const appInstalled = await Config.findOne({
-      ns: 'crowi',
       key: 'app:installed',
       key: 'app:installed',
     });
     });
     // exit if exists
     // exit if exists
@@ -38,7 +37,6 @@ module.exports = {
 
 
     if (userCount > 0) {
     if (userCount > 0) {
       await Config.create({
       await Config.create({
-        ns: 'crowi',
         key: 'app:installed',
         key: 'app:installed',
         value: true,
         value: true,
       });
       });
@@ -53,7 +51,6 @@ module.exports = {
 
 
     // remote 'app:siteUrl'
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({
-      ns: 'crowi',
       key: 'app:installed',
       key: 'app:installed',
     });
     });
 
 

+ 0 - 1
apps/app/src/migrations/20200512005851-remove-behavior-type.js

@@ -23,7 +23,6 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
-      ns: 'crowi',
       key: 'customize:behavior',
       key: 'customize:behavior',
       value: JSON.stringify('growi'),
       value: JSON.stringify('growi'),
     });
     });

+ 0 - 1
apps/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -45,7 +45,6 @@ module.exports = {
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
-      ns: 'crowi',
       key: 'customize:layout',
       key: 'customize:layout',
       value: JSON.stringify(insertLayoutType),
       value: JSON.stringify(insertLayoutType),
     });
     });

+ 0 - 2
apps/app/src/migrations/20200828024025-copy-aws-setting.js

@@ -25,7 +25,6 @@ module.exports = {
           insertOne: {
           insertOne: {
             document: {
             document: {
               key: 'mail:sesAccessKeyId',
               key: 'mail:sesAccessKeyId',
-              ns: 'crowi',
               value: accessKeyId.value,
               value: accessKeyId.value,
             },
             },
           },
           },
@@ -39,7 +38,6 @@ module.exports = {
           insertOne: {
           insertOne: {
             document: {
             document: {
               key: 'mail:sesSecretAccessKey',
               key: 'mail:sesSecretAccessKey',
-              ns: 'crowi',
               value: secretAccessKey.value,
               value: secretAccessKey.value,
             },
             },
           },
           },

+ 0 - 3
apps/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -13,11 +13,9 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const sesAccessKeyId = await Config.findOne({
     const sesAccessKeyId = await Config.findOne({
-      ns: 'crowi',
       key: 'mail:sesAccessKeyId',
       key: 'mail:sesAccessKeyId',
     });
     });
     const transmissionMethod = await Config.findOne({
     const transmissionMethod = await Config.findOne({
-      ns: 'crowi',
       key: 'mail:transmissionMethod',
       key: 'mail:transmissionMethod',
     });
     });
 
 
@@ -47,7 +45,6 @@ module.exports = {
 
 
     // remote 'mail:transmissionMethod'
     // remote 'mail:transmissionMethod'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({
-      ns: 'crowi',
       key: 'mail:transmissionMethod',
       key: 'mail:transmissionMethod',
     });
     });
 
 

+ 0 - 1
apps/app/src/migrations/20200903080025-remove-timeline-type.js.js

@@ -24,7 +24,6 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
-      ns: 'crowi',
       key: 'customize:isEnabledTimeline',
       key: 'customize:isEnabledTimeline',
       value: true,
       value: true,
     });
     });

+ 0 - 5
apps/app/src/migrations/20220311011114-convert-page-delete-config.js

@@ -15,7 +15,6 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const isNewConfigExists = await Config.count({
     const isNewConfigExists = await Config.count({
-      ns: 'crowi',
       key: 'security:pageDeletionAuthority',
       key: 'security:pageDeletionAuthority',
     }) > 0;
     }) > 0;
 
 
@@ -26,7 +25,6 @@ module.exports = {
     }
     }
 
 
     const oldConfig = await Config.findOne({
     const oldConfig = await Config.findOne({
-      ns: 'crowi',
       key: 'security:pageCompleteDeletionAuthority',
       key: 'security:pageCompleteDeletionAuthority',
     });
     });
 
 
@@ -37,17 +35,14 @@ module.exports = {
       await Config.insertMany(
       await Config.insertMany(
         [
         [
           {
           {
-            ns: 'crowi',
             key: 'security:pageDeletionAuthority',
             key: 'security:pageDeletionAuthority',
             value: oldValue,
             value: oldValue,
           },
           },
           {
           {
-            ns: 'crowi',
             key: 'security:pageRecursiveDeletionAuthority',
             key: 'security:pageRecursiveDeletionAuthority',
             value: `"${PageRecursiveDeleteConfigValue.Inherit}"`,
             value: `"${PageRecursiveDeleteConfigValue.Inherit}"`,
           },
           },
           {
           {
-            ns: 'crowi',
             key: 'security:pageRecursiveCompleteDeletionAuthority',
             key: 'security:pageRecursiveCompleteDeletionAuthority',
             value: `"${PageRecursiveDeleteCompConfigValue.Inherit}"`,
             value: `"${PageRecursiveDeleteCompConfigValue.Inherit}"`,
           },
           },

+ 0 - 1
apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js

@@ -23,7 +23,6 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
-      ns: 'crowi',
       key: 'customize:isSavedStatesOfTabChanges',
       key: 'customize:isSavedStatesOfTabChanges',
       value: false,
       value: false,
     });
     });

+ 0 - 1
apps/app/src/migrations/20230213090921-remove-presentation-configurations.js

@@ -24,7 +24,6 @@ module.exports = {
     await mongoose.connect(getMongoUri(), mongoOptions);
     await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
-      ns: 'crowi',
       key: 'markdown:presentation:pageBreakSeparator',
       key: 'markdown:presentation:pageBreakSeparator',
       value: 2,
       value: 2,
     });
     });

+ 48 - 43
apps/app/src/pages/[[...path]].page.tsx

@@ -175,7 +175,6 @@ type Props = CommonProps & {
   isAclEnabled: boolean,
   isAclEnabled: boolean,
   // hasSlackConfig: boolean,
   // hasSlackConfig: boolean,
   drawioUri: string | null,
   drawioUri: string | null,
-  noCdn: string,
   // highlightJsStyle: string,
   // highlightJsStyle: string,
   isAllReplyShown: boolean,
   isAllReplyShown: boolean,
   isContainerFluid: boolean,
   isContainerFluid: boolean,
@@ -233,7 +232,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   // useIsMailerSetup(props.isMailerSetup);
   // useIsMailerSetup(props.isMailerSetup);
   useIsAclEnabled(props.isAclEnabled);
   useIsAclEnabled(props.isAclEnabled);
   // useHasSlackConfig(props.hasSlackConfig);
   // useHasSlackConfig(props.hasSlackConfig);
-  // useNoCdn(props.noCdn);
   useDefaultIndentSize(props.adminPreferredIndentSize);
   useDefaultIndentSize(props.adminPreferredIndentSize);
   useIsIndentSizeForced(props.isIndentSizeForced);
   useIsIndentSizeForced(props.isIndentSizeForced);
   useDisableLinkSharing(props.disableLinkSharing);
   useDisableLinkSharing(props.disableLinkSharing);
@@ -476,24 +474,29 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
     }
     }
   }
   }
 
 
-  const pageWithMeta: IPageToShowRevisionWithMeta | null = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
-  const page = pageWithMeta?.data as unknown as PageDocument;
+  const pageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
+  const { data: page, meta } = pageWithMeta ?? {};
 
 
   // add user to seen users
   // add user to seen users
   if (page != null && user != null) {
   if (page != null && user != null) {
     await page.seen(user);
     await page.seen(user);
   }
   }
 
 
+  props.pageWithMeta = null;
+
   // populate & check if the revision is latest
   // populate & check if the revision is latest
   if (page != null) {
   if (page != null) {
     page.initLatestRevisionField(revisionId);
     page.initLatestRevisionField(revisionId);
     props.isLatestRevision = page.isLatestRevision();
     props.isLatestRevision = page.isLatestRevision();
-    const ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+    const ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
     props.skipSSR = await skipSSR(page, ssrMaxRevisionBodyLength);
     props.skipSSR = await skipSSR(page, ssrMaxRevisionBodyLength);
-    await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
-  }
+    const populatedPage = await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
 
 
-  props.pageWithMeta = pageWithMeta;
+    props.pageWithMeta = {
+      data: populatedPage,
+      meta,
+    };
+  }
 }
 }
 
 
 async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
@@ -558,64 +561,66 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
   const {
   const {
-    searchService, configManager, aclService,
+    configManager, searchService, aclService, fileUploadService,
+    slackIntegrationService, passportService,
   } = crowi;
   } = crowi;
 
 
-  props.aiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
+  props.aiEnabled = configManager.getConfig('app:aiEnabled');
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
-  props.elasticsearchMaxBodyLengthToIndex = configManager.getConfig('crowi', 'app:elasticsearchMaxBodyLengthToIndex');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.elasticsearchMaxBodyLengthToIndex = configManager.getConfig('app:elasticsearchMaxBodyLengthToIndex');
 
 
-  props.isRomUserAllowedToComment = configManager.getConfig('crowi', 'security:isRomUserAllowedToComment');
+  props.isRomUserAllowedToComment = configManager.getConfig('security:isRomUserAllowedToComment');
 
 
-  props.isSlackConfigured = crowi.slackIntegrationService.isSlackConfigured;
+  props.isSlackConfigured = slackIntegrationService.isSlackConfigured;
   // props.isMailerSetup = mailService.isMailerSetup;
   // props.isMailerSetup = mailService.isMailerSetup;
   props.isAclEnabled = aclService.isAclEnabled();
   props.isAclEnabled = aclService.isAclEnabled();
   // props.hasSlackConfig = slackNotificationService.hasSlackConfig();
   // props.hasSlackConfig = slackNotificationService.hasSlackConfig();
-  props.drawioUri = configManager.getConfig('crowi', 'app:drawioUri');
-  props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
-  // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
-  props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');
-  props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
-  props.isEnabledStaleNotification = configManager.getConfig('crowi', 'customize:isEnabledStaleNotification');
-  props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
-  props.isUploadAllFileAllowed = crowi.fileUploadService.getFileUploadEnabled();
-  props.isUploadEnabled = crowi.fileUploadService.getIsUploadable();
+  props.drawioUri = configManager.getConfig('app:drawioUri');
+  // props.highlightJsStyle = configManager.getConfig('customize:highlightJsStyle');
+  props.isAllReplyShown = configManager.getConfig('customize:isAllReplyShown');
+  props.isContainerFluid = configManager.getConfig('customize:isContainerFluid');
+  props.isEnabledStaleNotification = configManager.getConfig('customize:isEnabledStaleNotification');
+  props.disableLinkSharing = configManager.getConfig('security:disableLinkSharing');
+  props.isUploadAllFileAllowed = fileUploadService.getFileUploadEnabled();
+  props.isUploadEnabled = fileUploadService.getIsUploadable();
 
 
-  props.isLocalAccountRegistrationEnabled = crowi.passportService.isLocalStrategySetup
-  && configManager.getConfig('crowi', 'security:registrationMode') !== RegistrationMode.CLOSED;
+  props.isLocalAccountRegistrationEnabled = passportService.isLocalStrategySetup
+  && configManager.getConfig('security:registrationMode') !== RegistrationMode.CLOSED;
 
 
-  props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
-  props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
+  props.adminPreferredIndentSize = configManager.getConfig('markdown:adminPreferredIndentSize');
+  props.isIndentSizeForced = configManager.getConfig('markdown:isIndentSizeForced');
 
 
-  props.isEnabledAttachTitleHeader = configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader');
+  props.isEnabledAttachTitleHeader = configManager.getConfig('customize:isEnabledAttachTitleHeader');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
-    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
-    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
+    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
 
-    drawioUri: configManager.getConfig('crowi', 'app:drawioUri'),
-    plantumlUri: configManager.getConfig('crowi', 'app:plantumlUri'),
+    drawioUri: configManager.getConfig('app:drawioUri'),
+    plantumlUri: configManager.getConfig('app:plantumlUri'),
 
 
     // XSS Options
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
+    customTagWhitelist: configManager.getConfig('markdown:rehypeSanitize:tagNames'),
+    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
+      : undefined,
+    highlightJsStyleBorder: configManager.getConfig('customize:highlightJsStyleBorder'),
   };
   };
 
 
-  props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+  props.ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
 }
 }
 
 
 /**
 /**

+ 18 - 16
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -94,30 +94,32 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
-  props.isEnabledMarp = configManager.getConfig('crowi', 'customize:isEnabledMarp');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.isEnabledMarp = configManager.getConfig('customize:isEnabledMarp');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
-    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
-    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
+    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
 
-    drawioUri: configManager.getConfig('crowi', 'app:drawioUri'),
-    plantumlUri: configManager.getConfig('crowi', 'app:plantumlUri'),
+    drawioUri: configManager.getConfig('app:drawioUri'),
+    plantumlUri: configManager.getConfig('app:plantumlUri'),
 
 
     // XSS Options
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
+    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
+    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
+      : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
   };
   };
 }
 }
 
 

+ 19 - 17
apps/app/src/pages/_search.page.tsx

@@ -121,33 +121,35 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
-  props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.isContainerFluid = configManager.getConfig('customize:isContainerFluid');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
-    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
-    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
+    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
 
-    drawioUri: configManager.getConfig('crowi', 'app:drawioUri'),
-    plantumlUri: configManager.getConfig('crowi', 'app:plantumlUri'),
+    drawioUri: configManager.getConfig('app:drawioUri'),
+    plantumlUri: configManager.getConfig('app:plantumlUri'),
 
 
     // XSS Options
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
+    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
+    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
+      : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
   };
   };
 
 
-  props.showPageLimitationL = configManager.getConfig('crowi', 'customize:showPageLimitationL');
+  props.showPageLimitationL = configManager.getConfig('customize:showPageLimitationL');
 }
 }
 
 
 /**
 /**

+ 1 - 1
apps/app/src/pages/admin/ai-integration.page.tsx

@@ -50,7 +50,7 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const { crowi } = req;
   const { crowi } = req;
   const { configManager } = crowi;
   const { configManager } = crowi;
 
 
-  props.aiEnabled = configManager.getConfig('crowi', 'app:aiEnabled');
+  props.aiEnabled = configManager.getConfig('app:aiEnabled');
 };
 };
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

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

@@ -57,8 +57,8 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const { crowi } = req;
   const { crowi } = req;
   const { activityService } = crowi;
   const { activityService } = crowi;
 
 
-  props.auditLogEnabled = crowi.configManager.getConfig('crowi', 'app:auditLogEnabled');
-  props.activityExpirationSeconds = crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds');
+  props.auditLogEnabled = crowi.configManager.getConfig('app:auditLogEnabled');
+  props.activityExpirationSeconds = crowi.configManager.getConfig('app:activityExpirationSeconds');
   props.auditLogAvailableActions = activityService.getAvailableActions(false);
   props.auditLogAvailableActions = activityService.getAvailableActions(false);
 };
 };
 
 

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

@@ -12,6 +12,7 @@ import { Provider } from 'unstated';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
+import { configManager } from '~/server/service/config-manager';
 import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores-universal/context';
 import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores-universal/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -22,7 +23,7 @@ const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenP
 
 
 
 
 type Props = CommonProps & {
 type Props = CommonProps & {
-  customizeTitle: string,
+  customizeTitle?: string,
   isCustomizedLogoUploaded: boolean,
   isCustomizedLogoUploaded: boolean,
 };
 };
 
 
@@ -66,7 +67,7 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
 
 
-  props.customizeTitle = crowi.configManager.getConfig('crowi', 'customize:title');
+  props.customizeTitle = crowi.configManager.getConfig('customize:title');
   props.isCustomizedLogoUploaded = await crowi.attachmentService.isBrandLogoExist();
   props.isCustomizedLogoUploaded = await crowi.attachmentService.isBrandLogoExist();
 };
 };
 
 

+ 1 - 1
apps/app/src/pages/admin/data-transfer.page.tsx

@@ -60,7 +60,7 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
 
 
-  props.growiCloudUri = await crowi.configManager.getConfig('crowi', 'app:growiCloudUri');
+  props.growiCloudUri = await crowi.configManager.getConfig('app:growiCloudUri');
 };
 };
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 5 - 5
apps/app/src/pages/admin/index.page.tsx

@@ -25,12 +25,12 @@ const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenP
 
 
 
 
 type Props = CommonProps & {
 type Props = CommonProps & {
-  growiCloudUri: string,
-  growiAppIdForGrowiCloud: number,
+  growiCloudUri?: string,
+  growiAppIdForGrowiCloud?: number,
 };
 };
 
 
 
 
-const AdminHomepage: NextPage<Props> = (props) => {
+const AdminHomepage: NextPage<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiAppIdForGrowiCloud(props.growiAppIdForGrowiCloud);
   useGrowiAppIdForGrowiCloud(props.growiAppIdForGrowiCloud);
@@ -71,8 +71,8 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
 
 
-  props.growiCloudUri = await crowi.configManager.getConfig('crowi', 'app:growiCloudUri');
-  props.growiAppIdForGrowiCloud = await crowi.configManager.getConfig('crowi', 'app:growiAppIdForCloud');
+  props.growiCloudUri = crowi.configManager.getConfig('app:growiCloudUri');
+  props.growiAppIdForGrowiCloud = crowi.configManager.getConfig('app:growiAppIdForCloud');
 };
 };
 
 
 
 

+ 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();
 };
 };
 
 
 
 

+ 1 - 1
apps/app/src/pages/installer.page.tsx

@@ -79,7 +79,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   const { crowi } = req;
   const { crowi } = req;
   const { configManager } = crowi;
   const { configManager } = crowi;
 
 
-  props.minPasswordLength = configManager.getConfig('crowi', 'app:minPasswordLength');
+  props.minPasswordLength = configManager.getConfig('app:minPasswordLength');
 }
 }
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 11 - 12
apps/app/src/pages/login/index.page.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import React from 'react';
 
 
-import { IExternalAuthProviderType } from '@growi/core';
 import type {
 import type {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
@@ -13,6 +12,7 @@ import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import { isExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
 import { isExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
+import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import type { RegistrationMode } from '~/interfaces/registration-mode';
 import type { RegistrationMode } from '~/interfaces/registration-mode';
 import type { CommonProps } from '~/pages/utils/commons';
 import type { CommonProps } from '~/pages/utils/commons';
 import { getServerSideCommonProps, generateCustomTitle, getNextI18NextConfig } from '~/pages/utils/commons';
 import { getServerSideCommonProps, generateCustomTitle, getNextI18NextConfig } from '~/pages/utils/commons';
@@ -95,11 +95,10 @@ function injectEnabledStrategies(context: GetServerSidePropsContext, props: Prop
   } = crowi;
   } = crowi;
 
 
   props.enabledExternalAuthType = [
   props.enabledExternalAuthType = [
-    configManager.getConfig('crowi', 'security:passport-google:isEnabled') === true ? IExternalAuthProviderType.google : undefined,
-    configManager.getConfig('crowi', 'security:passport-github:isEnabled') === true ? IExternalAuthProviderType.github : undefined,
-    // configManager.getConfig('crowi', 'security:passport-facebook:isEnabled') ?? IExternalAuthProviderType.facebook : undefined,
-    configManager.getConfig('crowi', 'security:passport-saml:isEnabled') === true ? IExternalAuthProviderType.saml : undefined,
-    configManager.getConfig('crowi', 'security:passport-oidc:isEnabled') === true ? IExternalAuthProviderType.oidc : undefined,
+    configManager.getConfig('security:passport-google:isEnabled') === true ? IExternalAuthProviderType.google : undefined,
+    configManager.getConfig('security:passport-github:isEnabled') === true ? IExternalAuthProviderType.github : undefined,
+    configManager.getConfig('security:passport-saml:isEnabled') === true ? IExternalAuthProviderType.saml : undefined,
+    configManager.getConfig('security:passport-oidc:isEnabled') === true ? IExternalAuthProviderType.oidc : undefined,
 
 
   ]
   ]
     .filter((authType): authType is Exclude<typeof authType, undefined> => authType != null);
     .filter((authType): authType is Exclude<typeof authType, undefined> => authType != null);
@@ -114,15 +113,15 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
     passportService,
     passportService,
   } = crowi;
   } = crowi;
 
 
-  props.isPasswordResetEnabled = crowi.configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled');
+  props.isPasswordResetEnabled = configManager.getConfig('security:passport-local:isPasswordResetEnabled');
   props.isMailerSetup = mailService.isMailerSetup;
   props.isMailerSetup = mailService.isMailerSetup;
   props.isLocalStrategySetup = passportService.isLocalStrategySetup;
   props.isLocalStrategySetup = passportService.isLocalStrategySetup;
   props.isLdapStrategySetup = passportService.isLdapStrategySetup;
   props.isLdapStrategySetup = passportService.isLdapStrategySetup;
-  props.isLdapSetupFailed = configManager.getConfig('crowi', 'security:passport-ldap:isEnabled') && !props.isLdapStrategySetup;
-  props.registrationWhitelist = configManager.getConfig('crowi', 'security:registrationWhitelist');
-  props.isEmailAuthenticationEnabled = configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
-  props.registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
-  props.minPasswordLength = configManager.getConfig('crowi', 'app:minPasswordLength');
+  props.isLdapSetupFailed = configManager.getConfig('security:passport-ldap:isEnabled') && !props.isLdapStrategySetup;
+  props.registrationWhitelist = configManager.getConfig('security:registrationWhitelist');
+  props.isEmailAuthenticationEnabled = configManager.getConfig('security:passport-local:isEmailAuthenticationEnabled');
+  props.registrationMode = configManager.getConfig('security:registrationMode');
+  props.minPasswordLength = configManager.getConfig('app:minPasswordLength');
 }
 }
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 19 - 17
apps/app/src/pages/me/[[...path]].page.tsx

@@ -173,33 +173,35 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
 
 
-  props.registrationWhitelist = configManager.getConfig('crowi', 'security:registrationWhitelist');
+  props.registrationWhitelist = configManager.getConfig('security:registrationWhitelist');
 
 
-  props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
+  props.showPageLimitationXL = crowi.configManager.getConfig('customize:showPageLimitationXL');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
-    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
-    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
+    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
 
-    drawioUri: configManager.getConfig('crowi', 'app:drawioUri'),
-    plantumlUri: configManager.getConfig('crowi', 'app:plantumlUri'),
+    drawioUri: configManager.getConfig('app:drawioUri'),
+    plantumlUri: configManager.getConfig('app:plantumlUri'),
 
 
     // XSS Options
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
+    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
+    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
+      : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
   };
   };
 }
 }
 
 

+ 28 - 23
apps/app/src/pages/share/[[...path]].page.tsx

@@ -18,7 +18,7 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
-import type { PageDocument } from '~/server/models/page';
+import type { PageDocument, PageModel } from '~/server/models/page';
 import ShareLink from '~/server/models/share-link';
 import ShareLink from '~/server/models/share-link';
 import {
 import {
   useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
   useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
@@ -156,37 +156,39 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   const { crowi } = req;
   const { crowi } = req;
   const { configManager, searchService } = crowi;
   const { configManager, searchService } = crowi;
 
 
-  props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
+  props.disableLinkSharing = configManager.getConfig('security:disableLinkSharing');
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
 
 
-  props.drawioUri = configManager.getConfig('crowi', 'app:drawioUri');
+  props.drawioUri = configManager.getConfig('app:drawioUri');
 
 
   props.isLocalAccountRegistrationEnabled = crowi.passportService.isLocalStrategySetup
   props.isLocalAccountRegistrationEnabled = crowi.passportService.isLocalStrategySetup
-    && configManager.getConfig('crowi', 'security:registrationMode') !== RegistrationMode.CLOSED;
+    && configManager.getConfig('security:registrationMode') !== RegistrationMode.CLOSED;
 
 
   props.rendererConfig = {
   props.rendererConfig = {
     isSharedPage: true,
     isSharedPage: true,
-    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
-    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
-    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
+    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
 
-    drawioUri: configManager.getConfig('crowi', 'app:drawioUri'),
-    plantumlUri: configManager.getConfig('crowi', 'app:plantumlUri'),
+    drawioUri: configManager.getConfig('app:drawioUri'),
+    plantumlUri: configManager.getConfig('app:plantumlUri'),
 
 
     // XSS Options
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
-    sanitizeType: configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
-    customAttrWhitelist: JSON.parse(crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:attributes')),
-    customTagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
-    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
+    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
+    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
+      : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
   };
   };
 
 
-  props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+  props.ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
 }
 }
 
 
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
@@ -229,13 +231,16 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
       props.shareLink = shareLink.toObject();
       props.shareLink = shareLink.toObject();
 
 
       // retrieve Page
       // retrieve Page
-      const Page = crowi.model('Page');
+      const Page = crowi.model('Page') as PageModel;
       const relatedPage = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
       const relatedPage = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
       // determine whether skip SSR
       // determine whether skip SSR
-      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
-      props.skipSSR = await skipSSR(relatedPage, ssrMaxRevisionBodyLength);
-      // populate
-      props.shareLinkRelatedPage = await relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
+      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('app:ssrMaxRevisionBodyLength');
+
+      if (relatedPage != null) {
+        props.skipSSR = await skipSSR(relatedPage, ssrMaxRevisionBodyLength);
+        // populate
+        props.shareLinkRelatedPage = await relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
+      }
     }
     }
   }
   }
   catch (err) {
   catch (err) {

+ 3 - 3
apps/app/src/pages/tags.page.tsx

@@ -152,11 +152,11 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
 }
 }

+ 4 - 4
apps/app/src/pages/trash.page.tsx

@@ -117,12 +117,12 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
-  props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.showPageLimitationXL = crowi.configManager.getConfig('customize:showPageLimitationXL');
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
 }
 }

+ 2 - 2
apps/app/src/pages/user-activation.page.tsx

@@ -81,8 +81,8 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   if (typeof context.query.errorCode === 'string') {
   if (typeof context.query.errorCode === 'string') {
     props.errorCode = context.query.errorCode as UserActivationErrorCode;
     props.errorCode = context.query.errorCode as UserActivationErrorCode;
   }
   }
-  props.registrationMode = req.crowi.configManager.getConfig('crowi', 'security:registrationMode');
-  props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
+  props.registrationMode = req.crowi.configManager.getConfig('security:registrationMode');
+  props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('security:passport-local:isEmailAuthenticationEnabled');
 
 
   await injectNextI18NextConfigurations(context, props, ['translation']);
   await injectNextI18NextConfigurations(context, props, ['translation']);
 
 

+ 9 - 8
apps/app/src/pages/utils/commons.ts

@@ -17,12 +17,13 @@ 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
   currentPathname: string,
   currentPathname: string,
   appTitle: string,
   appTitle: string,
-  siteUrl: string,
+  siteUrl: string | undefined,
   confidential: string,
   confidential: string,
   customTitleTemplate: string,
   customTitleTemplate: string,
   csrfToken: string,
   csrfToken: string,
@@ -31,7 +32,7 @@ export type CommonProps = {
   isMaintenanceMode: boolean,
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   redirectDestination: string | null,
   isDefaultLogo: boolean,
   isDefaultLogo: boolean,
-  growiCloudUri: string,
+  growiCloudUri: string | undefined,
   isAccessDeniedForNonAdminUser?: boolean,
   isAccessDeniedForNonAdminUser?: boolean,
   currentUser?: IUserHasId,
   currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
   forcedColorScheme?: ColorScheme,
@@ -74,7 +75,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   }
   }
 
 
   const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
   const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
-  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
+  const isDefaultLogo = crowi.configManager.getConfig('customize:isDefaultLogo') || !isCustomizedLogoUploaded;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;
 
 
   // retrieve UserUISett ings
   // retrieve UserUISett ings
@@ -87,18 +88,18 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     namespacesRequired: ['translation'],
     namespacesRequired: ['translation'],
     currentPathname,
     currentPathname,
     appTitle: appService.getAppTitle(),
     appTitle: appService.getAppTitle(),
-    siteUrl: configManager.getConfig('crowi', 'app:siteUrl'), // DON'T USE appService.getSiteUrl()
+    siteUrl: configManager.getConfig('app:siteUrl'), // DON'T USE appService.getSiteUrl()
     confidential: appService.getAppConfidential() || '',
     confidential: appService.getAppConfidential() || '',
     customTitleTemplate: customizeService.customTitleTemplate,
     customTitleTemplate: customizeService.customTitleTemplate,
     csrfToken: req.csrfToken(),
     csrfToken: req.csrfToken(),
-    isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,
-    growiVersion: crowi.version,
+    isContainerFluid: configManager.getConfig('customize:isContainerFluid') ?? false,
+    growiVersion: getGrowiVersion(),
     isMaintenanceMode,
     isMaintenanceMode,
     redirectDestination,
     redirectDestination,
     currentUser,
     currentUser,
     isDefaultLogo,
     isDefaultLogo,
     forcedColorScheme,
     forcedColorScheme,
-    growiCloudUri: configManager.getConfig('crowi', 'app:growiCloudUri'),
+    growiCloudUri: configManager.getConfig('app:growiCloudUri'),
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
   };
   };
 
 
@@ -122,7 +123,7 @@ export const getLangAtServerSide = (req: CrowiRequest): Lang => {
   const { configManager } = req.crowi;
   const { configManager } = req.crowi;
 
 
   return user == null ? detectLocaleFromBrowserAcceptLanguage(headers)
   return user == null ? detectLocaleFromBrowserAcceptLanguage(headers)
-    : (user.lang ?? configManager.getConfig('crowi', 'app:globalLang') as Lang ?? Lang.en_US) ?? Lang.en_US;
+    : (user.lang ?? configManager.getConfig('app:globalLang') ?? Lang.en_US) ?? Lang.en_US;
 };
 };
 
 
 // use this function to get locale for html lang attribute
 // use this function to get locale for html lang attribute

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

@@ -1,10 +1,9 @@
-import Logger from 'bunyan';
+import type Logger from 'bunyan';
 
 
+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';
 
 
-import Crowi from './crowi';
-
 const logger: Logger = loggerFactory('growi');
 const logger: Logger = loggerFactory('growi');
 
 
 
 
@@ -21,10 +20,15 @@ process.on('unhandledRejection', (reason, p) => {
 
 
 async function main() {
 async function main() {
   try {
   try {
-    // eslint-disable-next-line @typescript-eslint/no-var-requires
+    // start OpenTelemetry
+    await startInstrumentation();
+
+    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(() => {

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

@@ -12,8 +12,7 @@ const logger = loggerFactory('growi:crowi:dev');
 class CrowiDev {
 class CrowiDev {
 
 
   /**
   /**
-   * Creates an instance of CrowiDev.
-   * @param {Crowi} crowi
+   * @param {import('~/server/crowi').default} crowi Crowi instance
    *
    *
    * @memberOf CrowiDev
    * @memberOf CrowiDev
    */
    */

+ 5 - 4
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');
@@ -39,9 +40,9 @@ module.exports = function(crowi, app) {
 
 
   const { configManager } = crowi;
   const { configManager } = crowi;
 
 
-  const trustProxyBool = configManager.getConfig('crowi', 'security:trustProxyBool');
-  const trustProxyCsv = configManager.getConfig('crowi', 'security:trustProxyCsv');
-  const trustProxyHops = configManager.getConfig('crowi', 'security:trustProxyHops');
+  const trustProxyBool = configManager.getConfig('security:trustProxyBool');
+  const trustProxyCsv = configManager.getConfig('security:trustProxyCsv');
+  const trustProxyHops = configManager.getConfig('security:trustProxyHops');
 
 
   const trustProxy = trustProxyBool ?? trustProxyCsv ?? trustProxyHops;
   const trustProxy = trustProxyBool ?? trustProxyCsv ?? trustProxyHops;
 
 
@@ -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;
 
 

+ 55 - 17
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';
 
 
@@ -58,23 +57,56 @@ class Crowi {
    */
    */
   accessTokenParser;
   accessTokenParser;
 
 
+  /** @type {ReturnType<typeof next>} */
+  nextApp;
+
+  /** @type {import('../service/config-manager').IConfigManagerForApp} */
+  configManager;
+
+  /** @type {import('../service/acl').AclService} */
+  aclService;
+
   /** @type {AppService} */
   /** @type {AppService} */
   appService;
   appService;
 
 
+  /** @type {FileUploader} */
+  fileUploadService;
+
+  /** @type {import('../service/growi-info').GrowiInfoService} */
+  growiInfoService;
+
   /** @type {import('../service/page').IPageService} */
   /** @type {import('../service/page').IPageService} */
   pageService;
   pageService;
 
 
-  /** @type UserNotificationService */
-  userNotificationService;
+  /** @type {import('../service/page-grant').default} */
+  pageGrantService;
 
 
-  /** @type {FileUploader} */
-  fileUploadService;
+  /** @type {import('../service/page-operation').default} */
+  pageOperationService;
+
+  /** @type {PassportService} */
+  passportService;
+
+  /** @type {QuestionnaireService} */
+  questionnaireService;
+
+  /** @type {QuestionnaireCronService} */
+  questionnaireCronService;
+
+  /** @type {SearchService} */
+  searchService;
+
+  /** @type {SlackIntegrationService} */
+  slackIntegrationService;
 
 
   /** @type {SocketIoService} */
   /** @type {SocketIoService} */
   socketIoService;
   socketIoService;
 
 
+  /** @type 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;
@@ -156,6 +188,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(),
@@ -254,7 +287,7 @@ Crowi.prototype.setupDatabase = function() {
 
 
 Crowi.prototype.setupSessionConfig = async function() {
 Crowi.prototype.setupSessionConfig = async function() {
   const session = require('express-session');
   const session = require('express-session');
-  const sessionMaxAge = this.configManager.getConfig('crowi', 'security:sessionMaxAge') || 2592000000; // default: 30days
+  const sessionMaxAge = this.configManager.getConfig('security:sessionMaxAge') || 2592000000; // default: 30days
   const redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null;
   const redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null;
   const uid = require('uid-safe').sync;
   const uid = require('uid-safe').sync;
 
 
@@ -394,8 +427,8 @@ Crowi.prototype.setupMailer = async function() {
 };
 };
 
 
 Crowi.prototype.autoInstall = async function() {
 Crowi.prototype.autoInstall = async function() {
-  const isInstalled = this.configManager.getConfig('crowi', 'app:installed');
-  const username = this.configManager.getConfig('crowi', 'autoInstall:adminUsername');
+  const isInstalled = this.configManager.getConfig('app:installed');
+  const username = this.configManager.getConfig('autoInstall:adminUsername');
 
 
   if (isInstalled || username == null) {
   if (isInstalled || username == null) {
     return;
     return;
@@ -405,14 +438,14 @@ Crowi.prototype.autoInstall = async function() {
 
 
   const firstAdminUserToSave = {
   const firstAdminUserToSave = {
     username,
     username,
-    name: this.configManager.getConfig('crowi', 'autoInstall:adminName'),
-    email: this.configManager.getConfig('crowi', 'autoInstall:adminEmail'),
-    password: this.configManager.getConfig('crowi', 'autoInstall:adminPassword'),
+    name: this.configManager.getConfig('autoInstall:adminName'),
+    email: this.configManager.getConfig('autoInstall:adminEmail'),
+    password: this.configManager.getConfig('autoInstall:adminPassword'),
     admin: true,
     admin: true,
   };
   };
-  const globalLang = this.configManager.getConfig('crowi', 'autoInstall:globalLang');
-  const allowGuestMode = this.configManager.getConfig('crowi', 'autoInstall:allowGuestMode');
-  const serverDate = this.configManager.getConfig('crowi', 'autoInstall:serverDate');
+  const globalLang = this.configManager.getConfig('autoInstall:globalLang');
+  const allowGuestMode = this.configManager.getConfig('autoInstall:allowGuestMode');
+  const serverDate = this.configManager.getConfig('autoInstall:serverDate');
 
 
   const installerService = new InstallerService(this);
   const installerService = new InstallerService(this);
 
 
@@ -610,7 +643,7 @@ Crowi.prototype.setUpApp = async function() {
     this.appService = new AppService(this);
     this.appService = new AppService(this);
 
 
     // add as a message handler
     // add as a message handler
-    const isInstalled = this.configManager.getConfig('crowi', 'app:installed');
+    const isInstalled = this.configManager.getConfig('app:installed');
     if (this.s2sMessagingService != null && !isInstalled) {
     if (this.s2sMessagingService != null && !isInstalled) {
       this.s2sMessagingService.addMessageHandler(this.appService);
       this.s2sMessagingService.addMessageHandler(this.appService);
     }
     }
@@ -638,6 +671,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
  */
  */

+ 2 - 1
apps/app/src/server/events/admin.js

@@ -1,6 +1,7 @@
-const util = require('util');
 const events = require('events');
 const events = require('events');
+const util = require('util');
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 function AdminEvent(crowi) {
 function AdminEvent(crowi) {
   this.crowi = crowi;
   this.crowi = crowi;
 
 

+ 1 - 0
apps/app/src/server/events/bookmark.js

@@ -1,6 +1,7 @@
 const events = require('events');
 const events = require('events');
 const util = require('util');
 const util = require('util');
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 function BookmarkEvent(crowi) {
 function BookmarkEvent(crowi) {
   this.crowi = crowi;
   this.crowi = crowi;
 
 

+ 1 - 0
apps/app/src/server/events/page.js

@@ -5,6 +5,7 @@ import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:events:page');
 const logger = loggerFactory('growi:events:page');
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 function PageEvent(crowi) {
 function PageEvent(crowi) {
   this.crowi = crowi;
   this.crowi = crowi;
 
 

+ 2 - 1
apps/app/src/server/events/tag.js

@@ -1,6 +1,7 @@
-const util = require('util');
 const events = require('events');
 const events = require('events');
+const util = require('util');
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 function TagEvent(crowi) {
 function TagEvent(crowi) {
   this.crowi = crowi;
   this.crowi = crowi;
 
 

+ 2 - 2
apps/app/src/server/middlewares/add-activity.ts

@@ -1,5 +1,5 @@
 import type { IUserHasId } from '@growi/core';
 import type { IUserHasId } from '@growi/core';
-import { NextFunction, Request, Response } from 'express';
+import type { NextFunction, Request, Response } from 'express';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import Activity from '~/server/models/activity';
@@ -12,7 +12,7 @@ interface AuthorizedRequest extends Request {
   user?: IUserHasId
   user?: IUserHasId
 }
 }
 
 
-export const generateAddActivityMiddleware = crowi => async(req: AuthorizedRequest, res: Response, next: NextFunction): Promise<void> => {
+export const generateAddActivityMiddleware = () => async(req: AuthorizedRequest, res: Response, next: NextFunction): Promise<void> => {
   if (req.method === 'GET') {
   if (req.method === 'GET') {
     logger.warn('This middleware is not available for GET requests');
     logger.warn('This middleware is not available for GET requests');
     return next();
     return next();

+ 1 - 0
apps/app/src/server/middlewares/admin-required.js

@@ -2,6 +2,7 @@ import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:middleware:admin-required');
 const logger = loggerFactory('growi:middleware:admin-required');
 
 
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi, fallback = null) => {
 module.exports = (crowi, fallback = null) => {
 
 
   return function(req, res, next) {
   return function(req, res, next) {

+ 1 - 0
apps/app/src/server/middlewares/application-installed.js

@@ -1,3 +1,4 @@
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const { appService } = crowi;
   const { appService } = crowi;
 
 

+ 1 - 0
apps/app/src/server/middlewares/application-not-installed.js

@@ -1,3 +1,4 @@
+/** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const { appService } = crowi;
   const { appService } = crowi;
 
 

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