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

Merge branch 'master' into support/obsolete-route-for-attachment

Yuki Takei 2 лет назад
Родитель
Сommit
554d18d3bb
35 измененных файлов с 1411 добавлено и 99 удалено
  1. 1 1
      apps/app/.env.test
  2. 6 2
      apps/app/package.json
  3. 7 0
      apps/app/public/static/locales/en_US/admin.json
  4. 7 0
      apps/app/public/static/locales/ja_JP/admin.json
  5. 7 0
      apps/app/public/static/locales/zh_CN/admin.json
  6. 78 0
      apps/app/src/client/services/AdminAppContainer.js
  7. 211 0
      apps/app/src/components/Admin/App/AzureSetting.tsx
  8. 75 2
      apps/app/src/components/Admin/App/FileUploadSetting.tsx
  9. 12 0
      apps/app/src/components/Admin/App/MaskedInput.module.scss
  10. 43 0
      apps/app/src/components/Admin/App/MaskedInput.tsx
  11. 1 0
      apps/app/src/features/questionnaire/interfaces/growi-info.ts
  12. 14 0
      apps/app/src/interfaces/res/admin/app-settings.ts
  13. 40 1
      apps/app/src/server/routes/apiv3/app-settings.js
  14. 48 0
      apps/app/src/server/service/config-loader.ts
  15. 12 0
      apps/app/src/server/service/config-manager.ts
  16. 260 0
      apps/app/src/server/service/file-uploader/azure.ts
  17. 1 0
      apps/app/src/server/service/file-uploader/index.js
  18. 10 0
      apps/app/src/server/service/g2g-transfer.ts
  19. 5 0
      apps/app/test-with-vite/download-mongo-binary/index.spec.ts
  20. 15 0
      apps/app/test-with-vite/download-mongo-binary/vitest.config.ts
  21. 4 0
      apps/app/test-with-vite/package.json
  22. 1 1
      apps/app/test-with-vite/setup/mongoms.ts
  23. 3 3
      package.json
  24. 1 1
      packages/core/package.json
  25. 1 1
      packages/hackmd/package.json
  26. 1 1
      packages/presentation/package.json
  27. 1 1
      packages/preset-templates/package.json
  28. 1 1
      packages/preset-themes/package.json
  29. 1 1
      packages/remark-attachment-refs/package.json
  30. 1 1
      packages/remark-drawio/package.json
  31. 1 1
      packages/remark-growi-directive/package.json
  32. 1 1
      packages/remark-lsx/package.json
  33. 1 1
      packages/slack/package.json
  34. 1 1
      packages/ui/package.json
  35. 539 78
      yarn.lock

+ 1 - 1
apps/app/.env.test

@@ -5,5 +5,5 @@
 ## > To prevent accidentally leaking env variables to the client, only variables prefixed with
 ## > VITE_ are exposed to your Vite-processed code. e.g. for the following env variables:
 ##
-VITE_MONGOMS_VERSION="6.0.6"
+VITE_MONGOMS_VERSION="6.0.9"
 # VITE_MONGOMS_DEBUG=1

+ 6 - 2
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -40,6 +40,7 @@
     "reg:run": "reg-suit run",
     "vitest:run": "vitest run config src --coverage",
     "vitest:run:integ": "vitest run -c vitest.config.integ.ts src --coverage",
+    "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
     "console": "yarn cross-env NODE_ENV=development yarn ts-node --experimental-repl-await src/server/console.js",
     "swagger-jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js",
@@ -58,6 +59,8 @@
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "^3.58.0",
     "@aws-sdk/s3-request-presigner": "^3.58.0",
+    "@azure/identity": "^3.3.2",
+    "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
@@ -218,6 +221,7 @@
     "@types/express": "^4.17.11",
     "@types/jest": "^29.5.2",
     "@types/react-scroll": "^1.8.4",
+    "@vitest/coverage-v8": "^0.34.6",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "bootstrap": "^4.6.1",
@@ -239,7 +243,7 @@
     "jquery": "^3.7.0",
     "load-css-file": "^1.0.0",
     "material-icons": "^1.11.3",
-    "mongodb-memory-server": "^8.15.1",
+    "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",
     "penpal": "^4.0.0",

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

@@ -381,6 +381,13 @@
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
+    "azure_label": "Azure(Blob)",
+    "azure_tenant_id": "Tenant ID",
+    "azure_client_id": "Client ID",
+    "azure_client_secret": "Client Secret",
+    "azure_storage_account_name": "Storage Account Name",
+    "azure_storage_container_name": "Container Name",
+    "azure_note_for_the_only_env_option": "The Azure Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "test_connection": "Test connection to mail",
     "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",

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

@@ -388,6 +388,13 @@
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
+    "azure_label": "Azure(Blob)",
+    "azure_tenant_id": "テナントID",
+    "azure_client_id": "クライアントID",
+    "azure_client_secret": "クライアントシークレット",
+    "azure_storage_account_name": "ストレージアカウント名",
+    "azure_storage_container_name": "コンテナ名",
+    "azure_note_for_the_only_env_option": "現在Azure設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
     "fixed_by_env_var": "環境変数 <code>FILE_UPLOAD={{fileUploadType}}</code> により固定されています。",
     "file_upload": "ファイルをアップロードするための設定を行います。ファイルアップロードの設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
     "test_connection": "接続テスト",

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

@@ -388,6 +388,13 @@
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
+    "azure_label": "Azure(Blob)",
+    "azure_tenant_id": "Tenant ID",
+    "azure_client_id": "Client ID",
+    "azure_client_secret": "Client Secret",
+    "azure_storage_account_name": "Storage Account Name",
+    "azure_storage_container_name": "Container Name",
+    "azure_note_for_the_only_env_option": "The Azure Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
     "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "test_connection": "测试邮件服务器连接",

+ 78 - 0
apps/app/src/client/services/AdminAppContainer.js

@@ -60,6 +60,19 @@ export default class AdminAppContainer extends Container {
       s3SecretAccessKey: '',
       s3ReferenceFileWithRelayMode: false,
 
+      azureReferenceFileWithRelayMode: false,
+      azureUseOnlyEnvVars: false,
+      azureTenantId: '',
+      azureClientId: '',
+      azureClientSecret: '',
+      azureStorageAccountName: '',
+      azureStorageContainerName: '',
+      envAzureTenantId: '',
+      envAzureClientId: '',
+      envAzureClientSecret: '',
+      envAzureStorageAccountName: '',
+      envAzureStorageContainerName: '',
+
       isEnabledPlugins: true,
 
       isMaintenanceMode: false,
@@ -120,6 +133,20 @@ export default class AdminAppContainer extends Container {
       envGcsApiKeyJsonPath: appSettingsParams.envGcsApiKeyJsonPath,
       envGcsBucket: appSettingsParams.envGcsBucket,
       envGcsUploadNamespace: appSettingsParams.envGcsUploadNamespace,
+
+      azureUseOnlyEnvVars: appSettingsParams.azureUseOnlyEnvVars,
+      azureTenantId: appSettingsParams.azureTenantId,
+      azureClientId: appSettingsParams.azureClientId,
+      azureClientSecret: appSettingsParams.azureClientSecret,
+      azureStorageAccountName: appSettingsParams.azureStorageAccountName,
+      azureStorageContainerName: appSettingsParams.azureStorageContainerName,
+      azureReferenceFileWithRelayMode: appSettingsParams.azureReferenceFileWithRelayMode,
+      envAzureTenantId: appSettingsParams.envAzureTenantId,
+      envAzureClientId: appSettingsParams.envAzureClientId,
+      envAzureClientSecret: appSettingsParams.envAzureClientSecret,
+      envAzureStorageAccountName: appSettingsParams.envAzureStorageAccountName,
+      envAzureStorageContainerName: appSettingsParams.envAzureStorageContainerName,
+
       isEnabledPlugins: appSettingsParams.isEnabledPlugins,
       isMaintenanceMode: appSettingsParams.isMaintenanceMode,
     });
@@ -316,6 +343,48 @@ export default class AdminAppContainer extends Container {
     this.setState({ gcsReferenceFileWithRelayMode });
   }
 
+  /**
+   * Change azureReferenceFileWithRelayMode
+   */
+  changeAzureReferenceFileWithRelayMode(azureReferenceFileWithRelayMode) {
+    this.setState({ azureReferenceFileWithRelayMode });
+  }
+
+  /**
+   * Change azureTenantId
+   */
+  changeAzureTenantId(azureTenantId) {
+    this.setState({ azureTenantId });
+  }
+
+  /**
+   * Change azureClientId
+   */
+  changeAzureClientId(azureClientId) {
+    this.setState({ azureClientId });
+  }
+
+  /**
+   * Change azureClientSecret
+   */
+  changeAzureClientSecret(azureClientSecret) {
+    this.setState({ azureClientSecret });
+  }
+
+  /**
+   * Change azureStorageAccountName
+   */
+  changeAzureStorageAccountName(azureStorageAccountName) {
+    this.setState({ azureStorageAccountName });
+  }
+
+  /**
+   * Change azureStorageContainerName
+   */
+  changeAzureStorageContainerName(azureStorageContainerName) {
+    this.setState({ azureStorageContainerName });
+  }
+
   /**
    * Update app setting
    * @memberOf AdminAppContainer
@@ -430,6 +499,15 @@ export default class AdminAppContainer extends Container {
       requestParams.s3ReferenceFileWithRelayMode = this.state.s3ReferenceFileWithRelayMode;
     }
 
+    if (fileUploadType === 'azure') {
+      requestParams.azureTenantId = this.state.azureTenantId;
+      requestParams.azureClientId = this.state.azureClientId;
+      requestParams.azureClientSecret = this.state.azureClientSecret;
+      requestParams.azureStorageAccountName = this.state.azureStorageAccountName;
+      requestParams.azureStorageContainerName = this.state.azureStorageContainerName;
+      requestParams.azureReferenceFileWithRelayMode = this.state.azureReferenceFileWithRelayMode;
+    }
+
     const response = await apiv3Put('/app-settings/file-upload-setting', requestParams);
     const { responseParams } = response.data;
     return this.setState(responseParams);

+ 211 - 0
apps/app/src/components/Admin/App/AzureSetting.tsx

@@ -0,0 +1,211 @@
+import { useTranslation } from 'next-i18next';
+
+import MaskedInput from './MaskedInput';
+
+export type AzureSettingMoleculeProps = {
+  azureReferenceFileWithRelayMode
+  azureUseOnlyEnvVars
+  azureTenantId
+  azureClientId
+  azureClientSecret
+  azureStorageAccountName
+  azureStorageContainerName
+  envAzureTenantId?
+  envAzureClientId?
+  envAzureClientSecret?
+  envAzureStorageAccountName?
+  envAzureStorageContainerName?
+  onChangeAzureReferenceFileWithRelayMode: (val: boolean) => void
+  onChangeAzureTenantId: (val: string) => void
+  onChangeAzureClientId: (val: string) => void
+  onChangeAzureClientSecret: (val: string) => void
+  onChangeAzureStorageAccountName: (val: string) => void
+  onChangeAzureStorageContainerName: (val: string) => void
+};
+
+export const AzureSettingMolecule = (props: AzureSettingMoleculeProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  const {
+    azureReferenceFileWithRelayMode,
+    azureUseOnlyEnvVars,
+    azureTenantId,
+    azureClientId,
+    azureClientSecret,
+    azureStorageAccountName,
+    envAzureTenantId,
+    envAzureClientId,
+    envAzureClientSecret,
+    envAzureStorageAccountName,
+    azureStorageContainerName,
+    envAzureStorageContainerName,
+  } = props;
+
+  return (
+    <>
+
+      <div className="row form-group my-3">
+        <label className="text-left text-md-right col-md-3 col-form-label">
+          {t('admin:app_setting.file_delivery_method')}
+        </label>
+
+        <div className="col-md-6">
+          <div className="dropdown">
+            <button
+              className="btn btn-outline-secondary dropdown-toggle"
+              type="button"
+              id="ddAzureReferenceFileWithRelayMode"
+              data-toggle="dropdown"
+              aria-haspopup="true"
+              aria-expanded="true"
+            >
+              {azureReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_relay')}
+              {!azureReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_redirect')}
+            </button>
+            <div className="dropdown-menu" aria-labelledby="ddAzureReferenceFileWithRelayMode">
+              <button
+                className="dropdown-item"
+                type="button"
+                onClick={() => { props?.onChangeAzureReferenceFileWithRelayMode(true) }}
+              >
+                {t('admin:app_setting.file_delivery_method_relay')}
+              </button>
+              <button
+                className="dropdown-item"
+                type="button"
+                onClick={() => { props?.onChangeAzureReferenceFileWithRelayMode(false) }}
+              >
+                { t('admin:app_setting.file_delivery_method_redirect')}
+              </button>
+            </div>
+
+            <p className="form-text text-muted small">
+              {t('admin:app_setting.file_delivery_method_redirect_info')}
+              <br />
+              {t('admin:app_setting.file_delivery_method_relay_info')}
+            </p>
+          </div>
+        </div>
+      </div>
+
+      {azureUseOnlyEnvVars && (
+        <p
+          className="alert alert-info"
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: t('admin:app_setting.azure_note_for_the_only_env_option', { env: 'AZURE_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+        />
+      )}
+      <table className={`table settings-table ${azureUseOnlyEnvVars && 'use-only-env-vars'}`}>
+        <colgroup>
+          <col className="item-name" />
+          <col className="from-db" />
+          <col className="from-env-vars" />
+        </colgroup>
+        <thead>
+          <tr>
+            <th></th>
+            <th>Database</th>
+            <th>Environment variables</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <th>{t('admin:app_setting.azure_tenant_id')}</th>
+            <td>
+              <MaskedInput
+                name="azureTenantId"
+                readOnly={azureUseOnlyEnvVars}
+                defaultValue={azureTenantId}
+                onChange={e => props?.onChangeAzureTenantId(e.target.value)}
+              />
+            </td>
+            <td>
+              <MaskedInput name="envAzureTenantId" defaultValue={envAzureTenantId || ''} readOnly tabIndex={-1} />
+              <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
+                <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_TENANT_ID' }) }} />
+              </p>
+            </td>
+          </tr>
+          <tr>
+            <th>{t('admin:app_setting.azure_client_id')}</th>
+            <td>
+              <MaskedInput
+                name="azureClientId"
+                readOnly={azureUseOnlyEnvVars}
+                defaultValue={azureClientId}
+                onChange={e => props?.onChangeAzureClientId(e.target.value)}
+              />
+            </td>
+            <td>
+              <MaskedInput name="envAzureClientId" defaultValue={envAzureClientId || ''} readOnly tabIndex={-1} />
+              <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
+                <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_CLIENT_ID' }) }} />
+              </p>
+            </td>
+          </tr>
+          <tr>
+            <th>{t('admin:app_setting.azure_client_secret')}</th>
+            <td>
+              <MaskedInput
+                name="azureClientSecret"
+                readOnly={azureUseOnlyEnvVars}
+                defaultValue={azureClientSecret}
+                onChange={e => props?.onChangeAzureClientSecret(e.target.value)}
+              />
+            </td>
+            <td>
+              <MaskedInput name="envAzureClientSecret" defaultValue={envAzureClientSecret || ''} readOnly tabIndex={-1} />
+              <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
+                <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_CLIENT_SECRET' }) }} />
+              </p>
+            </td>
+          </tr>
+          <tr>
+            <th>{t('admin:app_setting.azure_storage_account_name')}</th>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                name="azureStorageAccountName"
+                readOnly={azureUseOnlyEnvVars}
+                defaultValue={azureStorageAccountName}
+                onChange={e => props?.onChangeAzureStorageAccountName(e.target.value)}
+              />
+            </td>
+            <td>
+              <input className="form-control" type="text" value={envAzureStorageAccountName || ''} readOnly tabIndex={-1} />
+              <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
+                <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_STORAGE_ACCOUNT_NAME' }) }} />
+              </p>
+            </td>
+          </tr>
+          <tr>
+            <th>{t('admin:app_setting.azure_storage_container_name')}</th>
+            <td>
+              <input
+                className="form-control"
+                type="text"
+                name="azureStorageContainerName"
+                readOnly={azureUseOnlyEnvVars}
+                defaultValue={azureStorageContainerName}
+                onChange={e => props?.onChangeAzureStorageContainerName(e.target.value)}
+              />
+            </td>
+            <td>
+              <input className="form-control" type="text" value={envAzureStorageContainerName || ''} readOnly tabIndex={-1} />
+              <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
+                <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_STORAGE_CONTAINER_NAME' }) }} />
+              </p>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+    </>
+  );
+};

+ 75 - 2
apps/app/src/components/Admin/App/FileUploadSetting.tsx

@@ -10,17 +10,20 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 import { AwsSettingMolecule } from './AwsSetting';
 import type { AwsSettingMoleculeProps } from './AwsSetting';
+import { AzureSettingMolecule } from './AzureSetting';
+import type { AzureSettingMoleculeProps } from './AzureSetting';
 import { GcsSettingMolecule } from './GcsSetting';
 import type { GcsSettingMoleculeProps } from './GcsSetting';
 
-const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'] as const;
+
+const fileUploadTypes = ['aws', 'gcs', 'azure', 'gridfs', 'local'] as const;
 
 type FileUploadSettingMoleculeProps = {
   fileUploadType: string
   isFixedFileUploadByEnvVar: boolean
   envFileUploadType?: string
   onChangeFileUploadType: (e: ChangeEvent, type: string) => void
-} & AwsSettingMoleculeProps & GcsSettingMoleculeProps;
+} & AwsSettingMoleculeProps & GcsSettingMoleculeProps & AzureSettingMoleculeProps;
 
 export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMoleculeProps): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
@@ -102,6 +105,28 @@ export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMol
           onChangeGcsUploadNamespace={props.onChangeGcsUploadNamespace}
         />
       )}
+      {props.fileUploadType === 'azure' && (
+        <AzureSettingMolecule
+          azureReferenceFileWithRelayMode={props.azureReferenceFileWithRelayMode}
+          azureUseOnlyEnvVars={props.azureUseOnlyEnvVars}
+          azureTenantId={props.azureTenantId}
+          azureClientId={props.azureClientId}
+          azureClientSecret={props.azureClientSecret}
+          azureStorageAccountName={props.azureStorageAccountName}
+          azureStorageContainerName={props.azureStorageContainerName}
+          envAzureStorageAccountName={props.envAzureStorageAccountName}
+          envAzureStorageContainerName={props.envAzureStorageContainerName}
+          envAzureTenantId={props.envAzureTenantId}
+          envAzureClientId={props.envAzureClientId}
+          envAzureClientSecret={props.envAzureClientSecret}
+          onChangeAzureReferenceFileWithRelayMode={props.onChangeAzureReferenceFileWithRelayMode}
+          onChangeAzureTenantId={props.onChangeAzureTenantId}
+          onChangeAzureClientId={props.onChangeAzureClientId}
+          onChangeAzureClientSecret={props.onChangeAzureClientSecret}
+          onChangeAzureStorageAccountName={props.onChangeAzureStorageAccountName}
+          onChangeAzureStorageContainerName={props.onChangeAzureStorageContainerName}
+        />
+      )}
     </>
   );
 });
@@ -124,6 +149,11 @@ const FileUploadSetting = (props: FileUploadSettingProps): JSX.Element => {
     gcsReferenceFileWithRelayMode, gcsUseOnlyEnvVars,
     gcsApiKeyJsonPath, envGcsApiKeyJsonPath, gcsBucket,
     envGcsBucket, gcsUploadNamespace, envGcsUploadNamespace,
+    azureReferenceFileWithRelayMode, azureUseOnlyEnvVars,
+    azureTenantId, azureClientId, azureClientSecret,
+    azureStorageAccountName, azureStorageContainerName,
+    envAzureTenantId, envAzureClientId, envAzureClientSecret,
+    envAzureStorageAccountName, envAzureStorageContainerName,
   } = adminAppContainer.state;
 
   const submitHandler = useCallback(async() => {
@@ -182,6 +212,31 @@ const FileUploadSetting = (props: FileUploadSettingProps): JSX.Element => {
     adminAppContainer.changeGcsUploadNamespace(val);
   }, [adminAppContainer]);
 
+  // Azure
+  const onChangeAzureReferenceFileWithRelayModeHandler = useCallback((val: boolean) => {
+    adminAppContainer.changeAzureReferenceFileWithRelayMode(val);
+  }, [adminAppContainer]);
+
+  const onChangeAzureTenantIdHandler = useCallback((val: string) => {
+    adminAppContainer.changeAzureTenantId(val);
+  }, [adminAppContainer]);
+
+  const onChangeAzureClientIdHandler = useCallback((val: string) => {
+    adminAppContainer.changeAzureClientId(val);
+  }, [adminAppContainer]);
+
+  const onChangeAzureClientSecretHandler = useCallback((val: string) => {
+    adminAppContainer.changeAzureClientSecret(val);
+  }, [adminAppContainer]);
+
+  const onChangeAzureStorageAccountNameHandler = useCallback((val: string) => {
+    adminAppContainer.changeAzureStorageAccountName(val);
+  }, [adminAppContainer]);
+
+  const onChangeAzureStorageContainerNameHandler = useCallback((val: string) => {
+    adminAppContainer.changeAzureStorageContainerName(val);
+  }, [adminAppContainer]);
+
   return (
     <>
       <FileUploadSettingMolecule
@@ -213,6 +268,24 @@ const FileUploadSetting = (props: FileUploadSettingProps): JSX.Element => {
         onChangeGcsApiKeyJsonPath={onChangeGcsApiKeyJsonPathHandler}
         onChangeGcsBucket={onChangeGcsBucketHandler}
         onChangeGcsUploadNamespace={onChangeGcsUploadNamespaceHandler}
+        azureReferenceFileWithRelayMode={azureReferenceFileWithRelayMode}
+        azureUseOnlyEnvVars={azureUseOnlyEnvVars}
+        azureTenantId={azureTenantId}
+        azureClientId={azureClientId}
+        azureClientSecret={azureClientSecret}
+        azureStorageAccountName={azureStorageAccountName}
+        azureStorageContainerName={azureStorageContainerName}
+        envAzureTenantId={envAzureTenantId}
+        envAzureClientId={envAzureClientId}
+        envAzureClientSecret={envAzureClientSecret}
+        envAzureStorageAccountName={envAzureStorageAccountName}
+        envAzureStorageContainerName={envAzureStorageContainerName}
+        onChangeAzureReferenceFileWithRelayMode={onChangeAzureReferenceFileWithRelayModeHandler}
+        onChangeAzureTenantId={onChangeAzureTenantIdHandler}
+        onChangeAzureClientId={onChangeAzureClientIdHandler}
+        onChangeAzureClientSecret={onChangeAzureClientSecretHandler}
+        onChangeAzureStorageAccountName={onChangeAzureStorageAccountNameHandler}
+        onChangeAzureStorageContainerName={onChangeAzureStorageContainerNameHandler}
       />
       <AdminUpdateButtonRow onClick={submitHandler} disabled={retrieveError != null} />
     </>

+ 12 - 0
apps/app/src/components/Admin/App/MaskedInput.module.scss

@@ -0,0 +1,12 @@
+.MaskedInput {
+  position: relative;
+  display: flex;
+}
+
+.PasswordReveal {
+  position: absolute;
+  top: 0rem;
+  right: 0.5rem;
+  left: auto;
+  font-size: 1.4rem;
+}

+ 43 - 0
apps/app/src/components/Admin/App/MaskedInput.tsx

@@ -0,0 +1,43 @@
+import { useState } from 'react';
+
+import styles from './MaskedInput.module.scss';
+
+type Props = {
+  name: string
+  readOnly: boolean
+  defaultValue: string
+  onChange?: (e: any) => void
+  tabIndex?: number | undefined
+};
+
+export default function MaskedInput(props: Props): JSX.Element {
+  const [passwordShown, setPasswordShown] = useState(false);
+  const togglePassword = () => {
+    setPasswordShown(!passwordShown);
+  };
+
+  const {
+    name, readOnly, defaultValue, onChange, tabIndex,
+  } = props;
+
+  return (
+    <div className={styles.MaskedInput}>
+      <input
+        className="form-control"
+        type={passwordShown ? 'text' : 'password'}
+        name={name}
+        readOnly={readOnly}
+        defaultValue={defaultValue}
+        onChange={onChange}
+        tabIndex={tabIndex}
+      />
+      <span onClick={togglePassword} className={styles.PasswordReveal}>
+        {passwordShown ? (
+          <i className="fa fa-eye" />
+        ) : (
+          <i className="fa fa-eye-slash" />
+        )}
+      </span>
+    </div>
+  );
+}

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

@@ -13,6 +13,7 @@ export const GrowiAttachmentType = {
   aws: 'aws',
   gcs: 'gcs',
   gcp: 'gcp',
+  azure: 'azure',
   gridfs: 'gridfs',
   mongo: 'mongo',
   mongodb: 'mongodb',

+ 14 - 0
apps/app/src/interfaces/res/admin/app-settings.ts

@@ -39,6 +39,20 @@ export type IResAppSettings = {
   envGcsBucket: string,
   envGcsUploadNamespace: string,
 
+  azureUseOnlyEnvVars: boolean,
+  azureTenantId: string,
+  azureClientId: string,
+  azureClientSecret: string,
+  azureStorageAccountName: string,
+  azureStorageContainerName: string,
+  azureReferenceFileWithRelayMode: string,
+
+  envAzureTenantId: string,
+  envAzureClientId: string,
+  envAzureClientSecret: string,
+  envAzureStorageAccountName: string,
+  envAzureStorageContainerName: string,
+
   isEnabledPlugins: boolean,
 
   isQuestionnaireEnabled: boolean,

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

@@ -184,7 +184,7 @@ module.exports = (crowi) => {
       body('sesSecretAccessKey').trim(),
     ],
     fileUploadSetting: [
-      body('fileUploadType').isIn(['aws', 'gcs', 'local', 'gridfs']),
+      body('fileUploadType').isIn(['aws', 'gcs', 'local', 'gridfs', 'azure']),
       body('gcsApiKeyJsonPath').trim(),
       body('gcsBucket').trim(),
       body('gcsUploadNamespace').trim(),
@@ -197,6 +197,13 @@ module.exports = (crowi) => {
       body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('s3SecretAccessKey').trim(),
       body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
+      body('azureTenantId').trim(),
+      body('azureClientId').trim(),
+      body('azureClientSecret').trim(),
+      body('azureStorageAccountName').trim(),
+      body('azureStorageStorageName').trim(),
+      body('azureReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
+
     ],
     questionnaireSettings: [
       body('isQuestionnaireEnabled').isBoolean(),
@@ -269,6 +276,20 @@ module.exports = (crowi) => {
       envGcsBucket: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:bucket'),
       envGcsUploadNamespace: crowi.configManager.getConfigFromEnvVars('crowi', 'gcs:uploadNamespace'),
 
+      azureUseOnlyEnvVars: crowi.configManager.getConfig('crowi', 'azure:useOnlyEnvVarsForSomeOptions'),
+      azureTenantId: crowi.configManager.getConfigFromDB('crowi', 'azure:tenantId'),
+      azureClientId: crowi.configManager.getConfigFromDB('crowi', 'azure:clientId'),
+      azureClientSecret: crowi.configManager.getConfigFromDB('crowi', 'azure:clientSecret'),
+      azureStorageAccountName: crowi.configManager.getConfigFromDB('crowi', 'azure:storageAccountName'),
+      azureStorageContainerName: crowi.configManager.getConfigFromDB('crowi', 'azure:storageContainerName'),
+      azureReferenceFileWithRelayMode: crowi.configManager.getConfig('crowi', 'azure:referenceFileWithRelayMode'),
+
+      envAzureTenantId: crowi.configManager.getConfigFromEnvVars('crowi', 'azure:tenantId'),
+      envAzureClientId: crowi.configManager.getConfigFromEnvVars('crowi', 'azure:clientId'),
+      envAzureClientSecret: crowi.configManager.getConfigFromEnvVars('crowi', 'azure:clientSecret'),
+      envAzureStorageAccountName: crowi.configManager.getConfigFromEnvVars('crowi', 'azure:storageAccountName'),
+      envAzureStorageContainerName: crowi.configManager.getConfigFromEnvVars('crowi', 'azure:storageContainerName'),
+
       isEnabledPlugins: crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins'),
 
       isQuestionnaireEnabled: crowi.configManager.getConfig('crowi', 'questionnaire:isQuestionnaireEnabled'),
@@ -648,6 +669,15 @@ module.exports = (crowi) => {
       requestParams['aws:referenceFileWithRelayMode'] = req.body.s3ReferenceFileWithRelayMode;
     }
 
+    if (fileUploadType === 'azure') {
+      requestParams['azure:tenantId'] = req.body.azureTenantId;
+      requestParams['azure:clientId'] = req.body.azureClientId;
+      requestParams['azure:clientSecret'] = req.body.azureClientSecret;
+      requestParams['azure:storageAccountName'] = req.body.azureStorageAccountName;
+      requestParams['azure:storageContainerName'] = req.body.azureStorageContainerName;
+      requestParams['azure:referenceFileWithRelayMode'] = req.body.azureReferenceFileWithRelayMode;
+    }
+
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams, true);
 
@@ -677,6 +707,15 @@ module.exports = (crowi) => {
         responseParams.s3AccessKeyId = crowi.configManager.getConfig('crowi', 'aws:s3AccessKeyId');
         responseParams.s3ReferenceFileWithRelayMode = crowi.configManager.getConfig('crowi', 'aws:referenceFileWithRelayMode');
       }
+
+      if (fileUploadType === 'azure') {
+        responseParams.azureTenantId = crowi.configManager.getConfig('crowi', 'azure:tenantId');
+        responseParams.azureClientId = crowi.configManager.getConfig('crowi', 'azure:clientId');
+        responseParams.azureClientSecret = crowi.configManager.getConfig('crowi', 'azure:clientSecret');
+        responseParams.azureStorageAccountName = crowi.configManager.getConfig('crowi', 'azure:storageAccountName');
+        responseParams.azureStorageContainerName = crowi.configManager.getConfig('crowi', 'azure:storageContainerName');
+        responseParams.azureReferenceFileWithRelayMode = crowi.configManager.getConfig('crowi', 'azure:referenceFileWithRelayMode');
+      }
       const parameters = { action: SupportedAction.ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
       return res.apiv3({ responseParams });

+ 48 - 0
apps/app/src/server/service/config-loader.ts

@@ -520,6 +520,54 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.BOOLEAN,
     default: false,
   },
+  AZURE_TENANT_ID: {
+    ns:      'crowi',
+    key:     'azure:tenantId',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AZURE_CLIENT_ID: {
+    ns:      'crowi',
+    key:     'azure:clientId',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AZURE_CLIENT_SECRET: {
+    ns:      'crowi',
+    key:     'azure:clientSecret',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AZURE_STORAGE_ACCOUNT_NAME: {
+    ns:      'crowi',
+    key:     'azure:storageAccountName',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AZURE_STORAGE_CONTAINER_NAME: {
+    ns:      'crowi',
+    key:     'azure:storageContainerName',
+    type:    ValueType.STRING,
+    default: null,
+  },
+  AZURE_LIFETIME_SEC_FOR_TEMPORARY_URL: {
+    ns:      'crowi',
+    key:     'azure:lifetimeSecForTemporaryUrl',
+    type:    ValueType.NUMBER,
+    default: 120,
+  },
+  AZURE_REFERENCE_FILE_WITH_RELAY_MODE: {
+    ns:      'crowi',
+    key:     'azure:referenceFileWithRelayMode',
+    type:    ValueType.BOOLEAN,
+    default: false,
+  },
+  AZURE_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
+    ns:      'crowi',
+    key:     'azure:useOnlyEnvVarsForSomeOptions',
+    type:    ValueType.BOOLEAN,
+    default: false,
+  },
   GROWI_CLOUD_URI: {
     ns:      'crowi',
     key:     'app:growiCloudUri',

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

@@ -36,6 +36,13 @@ const KEYS_FOR_GCS_USE_ONLY_ENV_OPTION = [
   'gcs:uploadNamespace',
 ];
 
+const KEYS_FOR_AZURE_USE_ONLY_ENV_OPTION = [
+  'azure:tenantId',
+  'azure:clientId',
+  'azure:clientSecret',
+  'azure:storageAccountName',
+  'azure:storageContainerName',
+];
 
 export interface ConfigManager {
   loadConfigs(): Promise<void>,
@@ -255,6 +262,11 @@ class ConfigManagerImpl implements ConfigManager, S2sMessageHandlable {
         KEYS_FOR_GCS_USE_ONLY_ENV_OPTION.includes(key)
         && this.searchOnlyFromEnvVarConfigs('crowi', 'gcs:useOnlyEnvVarsForSomeOptions')
       )
+      // azure option
+      || (
+        KEYS_FOR_AZURE_USE_ONLY_ENV_OPTION.includes(key)
+        && this.searchOnlyFromEnvVarConfigs('crowi', 'azure:useOnlyEnvVarsForSomeOptions')
+      )
     ));
   }
 

+ 260 - 0
apps/app/src/server/service/file-uploader/azure.ts

@@ -0,0 +1,260 @@
+import path from 'path';
+
+import { ClientSecretCredential, TokenCredential } from '@azure/identity';
+import {
+  BlobServiceClient,
+  BlobClient,
+  BlockBlobClient,
+  BlobDeleteOptions,
+  BlobDeleteIfExistsResponse,
+  BlockBlobUploadResponse,
+  ContainerClient,
+  generateBlobSASQueryParameters,
+  ContainerSASPermissions,
+  SASProtocol,
+  BlockBlobParallelUploadOptions,
+  BlockBlobUploadStreamOptions,
+} from '@azure/storage-blob';
+
+import loggerFactory from '~/utils/logger';
+
+import { configManager } from '../config-manager';
+
+import { AbstractFileUploader, type SaveFileParam } from './file-uploader';
+
+const urljoin = require('url-join');
+
+const logger = loggerFactory('growi:service:fileUploaderAzure');
+
+interface FileMeta {
+  name: string;
+  size: number;
+}
+
+type AzureConfig = {
+  accountName: string,
+  containerName: string,
+}
+
+class AzureFileUploader extends AbstractFileUploader {
+
+  /**
+   * @inheritdoc
+   */
+  override isValidUploadSettings(): boolean {
+    throw new Error('Method not implemented.');
+  }
+
+  /**
+   * @inheritdoc
+   */
+  override saveFile(param: SaveFileParam) {
+    throw new Error('Method not implemented.');
+  }
+
+  /**
+   * @inheritdoc
+   */
+  override deleteFiles() {
+    throw new Error('Method not implemented.');
+  }
+
+  /**
+   * @inheritdoc
+   */
+  override respond(res: Response, attachment: Response): void {
+    throw new Error('Method not implemented.');
+  }
+
+}
+
+module.exports = (crowi) => {
+  const lib = new AzureFileUploader(crowi);
+
+  function getAzureConfig(): AzureConfig {
+    return {
+      accountName: configManager.getConfig('crowi', 'azure:storageAccountName'),
+      containerName: configManager.getConfig('crowi', 'azure:storageContainerName'),
+    };
+  }
+
+  function getCredential(): TokenCredential {
+    const tenantId = configManager.getConfig('crowi', 'azure:tenantId');
+    const clientId = configManager.getConfig('crowi', 'azure:clientId');
+    const clientSecret = configManager.getConfig('crowi', 'azure:clientSecret');
+    return new ClientSecretCredential(tenantId, clientId, clientSecret);
+  }
+
+  async function getContainerClient(): Promise<ContainerClient> {
+    const { accountName, containerName } = getAzureConfig();
+    const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, getCredential());
+    return blobServiceClient.getContainerClient(containerName);
+  }
+
+  // Server creates User Delegation SAS Token for container
+  // https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blob-create-user-delegation-sas-javascript
+  async function getSasToken(lifetimeSec) {
+    const { accountName, containerName } = getAzureConfig();
+    const blobServiceClient = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, getCredential());
+
+    const now = Date.now();
+    const startsOn = new Date(now - 30 * 1000);
+    const expiresOn = new Date(now + lifetimeSec * 1000);
+    const userDelegationKey = await blobServiceClient.getUserDelegationKey(startsOn, expiresOn);
+
+    // https://github.com/Azure/azure-sdk-for-js/blob/d4d55f73/sdk/storage/storage-blob/src/ContainerSASPermissions.ts#L24
+    // r:read, a:add, c:create, w:write, d:delete, l:list
+    const containerPermissionsForAnonymousUser = 'rl';
+    const sasOptions = {
+      containerName,
+      permissions: ContainerSASPermissions.parse(containerPermissionsForAnonymousUser),
+      protocol: SASProtocol.HttpsAndHttp,
+      startsOn,
+      expiresOn,
+    };
+
+    const sasToken = generateBlobSASQueryParameters(sasOptions, userDelegationKey, accountName).toString();
+
+    return sasToken;
+  }
+
+  function getFilePathOnStorage(attachment) {
+    const dirName = (attachment.page != null) ? 'attachment' : 'user';
+    return urljoin(dirName, attachment.fileName);
+  }
+
+  lib.isValidUploadSettings = function() {
+    return configManager.getConfig('crowi', 'azure:storageAccountName') != null
+      && configManager.getConfig('crowi', 'azure:storageContainerName') != null;
+  };
+
+  lib.canRespond = function() {
+    return !configManager.getConfig('crowi', 'azure:referenceFileWithRelayMode');
+  };
+
+  (lib as any).respond = async function(res, attachment) {
+    const containerClient = await getContainerClient();
+    const filePath = getFilePathOnStorage(attachment);
+    const blockBlobClient: BlockBlobClient = await containerClient.getBlockBlobClient(filePath);
+    const lifetimeSecForTemporaryUrl = configManager.getConfig('crowi', 'azure:lifetimeSecForTemporaryUrl');
+
+    const sasToken = await getSasToken(lifetimeSecForTemporaryUrl);
+    const signedUrl = `${blockBlobClient.url}?${sasToken}`;
+
+    res.redirect(signedUrl);
+
+    try {
+      return attachment.cashTemporaryUrlByProvideSec(signedUrl, lifetimeSecForTemporaryUrl);
+    }
+    catch (err) {
+      logger.error(err);
+    }
+
+  };
+
+  (lib as any).deleteFile = async function(attachment) {
+    const filePath = getFilePathOnStorage(attachment);
+    const containerClient = await getContainerClient();
+    const blockBlobClient = await containerClient.getBlockBlobClient(filePath);
+    const options: BlobDeleteOptions = { deleteSnapshots: 'include' };
+    const blobDeleteIfExistsResponse: BlobDeleteIfExistsResponse = await blockBlobClient.deleteIfExists(options);
+    if (!blobDeleteIfExistsResponse.errorCode) {
+      logger.info(`deleted blob ${filePath}`);
+    }
+  };
+
+  (lib as any).deleteFiles = async function(attachments) {
+    if (!lib.getIsUploadable()) {
+      throw new Error('Azure is not configured.');
+    }
+    for await (const attachment of attachments) {
+      (lib as any).deleteFile(attachment);
+    }
+  };
+
+  (lib as any).uploadAttachment = async function(readStream, attachment) {
+    if (!lib.getIsUploadable()) {
+      throw new Error('Azure is not configured.');
+    }
+
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+    const filePath = getFilePathOnStorage(attachment);
+    const containerClient = await getContainerClient();
+    const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
+    const DEFAULT_BLOCK_BUFFER_SIZE_BYTES: number = 8 * 1024 * 1024; // 8MB
+    const DEFAULT_MAX_CONCURRENCY = 5;
+    const options: BlockBlobUploadStreamOptions = {
+      blobHTTPHeaders: {
+        blobContentType: attachment.fileFormat,
+        blobContentDisposition: `attachment;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
+      },
+    };
+    return blockBlobClient.uploadStream(readStream, DEFAULT_BLOCK_BUFFER_SIZE_BYTES, DEFAULT_MAX_CONCURRENCY, options);
+  };
+
+  lib.saveFile = async function({ filePath, contentType, data }) {
+    const containerClient = await getContainerClient();
+    const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
+    const options: BlockBlobParallelUploadOptions = {
+      blobHTTPHeaders: {
+        blobContentType: contentType,
+        blobContentDisposition: `attachment;filename*=UTF-8''${encodeURIComponent(path.basename(filePath))}`,
+      },
+    };
+    const blockBlobUploadResponse: BlockBlobUploadResponse = await blockBlobClient.upload(data, data.length, options);
+    if (blockBlobUploadResponse.errorCode) { throw new Error(blockBlobUploadResponse.errorCode) }
+    return;
+  };
+
+  (lib as any).findDeliveryFile = async function(attachment) {
+    if (!lib.getIsReadable()) {
+      throw new Error('Azure is not configured.');
+    }
+
+    const filePath = getFilePathOnStorage(attachment);
+    const containerClient = await getContainerClient();
+    const blobClient: BlobClient = containerClient.getBlobClient(filePath);
+    const downloadResponse = await blobClient.download();
+    if (downloadResponse.errorCode) {
+      logger.error(downloadResponse.errorCode);
+      throw new Error(downloadResponse.errorCode);
+    }
+    if (!downloadResponse?.readableStreamBody) {
+      throw new Error(`Coudn't get file from Azure for the Attachment (${filePath})`);
+    }
+
+    return downloadResponse.readableStreamBody;
+  };
+
+
+  (lib as any).checkLimit = async function(uploadFileSize) {
+    const maxFileSize = configManager.getConfig('crowi', 'app:maxFileSize');
+    const gcsTotalLimit = configManager.getConfig('crowi', 'app:fileUploadTotalLimit');
+    return lib.doCheckLimit(uploadFileSize, maxFileSize, gcsTotalLimit);
+  };
+
+  (lib as any).listFiles = async function() {
+    if (!lib.getIsReadable()) {
+      throw new Error('Azure is not configured.');
+    }
+
+    const files: FileMeta[] = [];
+    const containerClient = await getContainerClient();
+
+    for await (const blob of containerClient.listBlobsFlat({
+      includeMetadata: false,
+      includeSnapshots: false,
+      includeTags: false,
+      includeVersions: false,
+      prefix: '',
+    })) {
+      files.push(
+        { name: blob.name, size: blob.properties.contentLength || 0 },
+      );
+    }
+
+    return files;
+  };
+
+  return lib;
+};

+ 1 - 0
apps/app/src/server/service/file-uploader/index.js

@@ -11,6 +11,7 @@ const envToModuleMappings = {
   gridfs:  'gridfs',
   gcp:     'gcs',
   gcs:     'gcs',
+  azure:   'azure',
 };
 
 class FileUploadServiceFactory {

+ 10 - 0
apps/app/src/server/service/g2g-transfer.ts

@@ -39,6 +39,10 @@ const UPLOAD_CONFIG_KEYS = [
   'gcs:uploadNamespace',
   'gcs:referenceFileWithRelayMode',
   'gcs:useOnlyEnvVarsForSomeOptions',
+  'azure:storageAccountName',
+  'azure:storageContainerName',
+  'azure:referenceFileWithRelayMode',
+  'azure:useOnlyEnvVarsForSomeOptions',
 ] as const;
 
 /**
@@ -544,6 +548,8 @@ export class G2GTransferReceiverService implements Receiver {
       bucket: undefined,
       customEndpoint: undefined, // for S3
       uploadNamespace: undefined, // for GCS
+      accountName: undefined, // for Azure Blob
+      containerName: undefined,
     };
 
     // put storage location info to check storage identification
@@ -556,6 +562,10 @@ export class G2GTransferReceiverService implements Receiver {
         attachmentInfo.bucket = configManager.getConfig('crowi', 'gcs:bucket');
         attachmentInfo.uploadNamespace = configManager.getConfig('crowi', 'gcs:uploadNamespace');
         break;
+      case 'azure':
+        attachmentInfo.accountName = configManager.getConfig('crowi', 'azure:storageAccountName');
+        attachmentInfo.containerName = configManager.getConfig('crowi', 'azure:storageContainerName');
+        break;
       default:
     }
 

+ 5 - 0
apps/app/test-with-vite/download-mongo-binary/index.spec.ts

@@ -0,0 +1,5 @@
+describe('Download mongo-binary', () => {
+  it('should be success', () => {
+    expect(true).toBeTruthy();
+  });
+});

+ 15 - 0
apps/app/test-with-vite/download-mongo-binary/vitest.config.ts

@@ -0,0 +1,15 @@
+import { defineConfig, mergeConfig } from 'vitest/config';
+
+import configShared from '../../vitest.config';
+
+export default mergeConfig(
+  configShared,
+  defineConfig({
+    test: {
+      hookTimeout: 60000, // increased for downloading MongoDB binary file
+      setupFiles: [
+        './test-with-vite/setup/mongoms.ts',
+      ],
+    },
+  }),
+);

+ 4 - 0
apps/app/test-with-vite/package.json

@@ -0,0 +1,4 @@
+{
+  "$schame": "http://json-schema.org/schema",
+  "type": "module"
+}

+ 1 - 1
apps/app/test-with-vite/setup/mongoms.ts

@@ -1,4 +1,4 @@
-import { MongoMemoryServer } from 'mongodb-memory-server';
+import { MongoMemoryServer } from 'mongodb-memory-server-core';
 import mongoose from 'mongoose';
 
 import { mongoOptions } from '~/server/util/mongoose-utils';

+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -60,7 +60,7 @@
     "@typescript-eslint/eslint-plugin": "^5.59.7",
     "@typescript-eslint/parser": "^5.59.7",
     "@vitejs/plugin-react": "^4.0.3",
-    "@vitest/coverage-c8": "^0.31.1",
+    "@vitest/coverage-v8": "^0.34.6",
     "@vitest/ui": "^0.31.1",
     "cypress": "^13.3.0",
     "cypress-wait-until": "^2.0.1",
@@ -93,7 +93,7 @@
     "vite": "^4.4.0",
     "vite-plugin-dts": "^2.3.0",
     "vite-tsconfig-paths": "^4.2.0",
-    "vitest": "^0.31.4",
+    "vitest": "^0.34.6",
     "vitest-mock-extended": "^1.1.3"
   },
   "engines": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "type": "module",

+ 1 - 1
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/preset-templates/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/preset-templates",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "scripts": {
     "test": "vitest run",
     "version": "yarn version --no-git-tag-version --preid=RC"

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",

+ 1 - 1
packages/remark-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-attachment-refs",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "license": "MIT",
   "type": "module",
   "main": "dist/index.cjs",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.2.4-RC.0",
+  "version": "6.3.0-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

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


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