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

Merge branch 'master' into support/apply-nextjs-2

yuken 3 лет назад
Родитель
Сommit
9818a56979
38 измененных файлов с 655 добавлено и 240 удалено
  1. 32 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 2 2
      packages/app/docker/README.md
  5. 7 7
      packages/app/package.json
  6. 7 1
      packages/app/public/static/locales/en_US/admin/admin.json
  7. 6 0
      packages/app/public/static/locales/en_US/translation.json
  8. 7 1
      packages/app/public/static/locales/ja_JP/admin/admin.json
  9. 6 0
      packages/app/public/static/locales/ja_JP/translation.json
  10. 7 1
      packages/app/public/static/locales/zh_CN/admin/admin.json
  11. 6 0
      packages/app/public/static/locales/zh_CN/translation.json
  12. 4 0
      packages/app/src/components/Admin/Customize/Customize.jsx
  13. 185 0
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  14. 4 2
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  15. 1 1
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  16. 127 0
      packages/app/src/components/Common/ImageCropModal.tsx
  17. 0 125
      packages/app/src/components/Me/ImageCropModal.jsx
  18. 3 2
      packages/app/src/components/Me/ProfileImageSettings.tsx
  19. 0 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  20. 1 1
      packages/app/src/interfaces/activity.ts
  21. 44 0
      packages/app/src/migrations/20220613064207-add-attachment-type-to-existing-attachments.js
  22. 24 4
      packages/app/src/server/crowi/express-init.js
  23. 7 0
      packages/app/src/server/interfaces/attachment.ts
  24. 9 2
      packages/app/src/server/models/attachment.js
  25. 5 0
      packages/app/src/server/models/config.ts
  26. 106 3
      packages/app/src/server/routes/apiv3/customize-setting.js
  27. 6 5
      packages/app/src/server/routes/attachment.js
  28. 20 65
      packages/app/src/server/routes/login-passport.js
  29. 4 3
      packages/app/src/server/service/attachment.js
  30. 14 2
      packages/app/src/server/service/config-loader.ts
  31. 1 1
      packages/codemirror-textlint/package.json
  32. 1 1
      packages/core/package.json
  33. 1 1
      packages/plugin-attachment-refs/package.json
  34. 1 1
      packages/plugin-lsx/package.json
  35. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  36. 1 1
      packages/slack/package.json
  37. 2 2
      packages/slackbot-proxy/package.json
  38. 1 1
      packages/ui/package.json

+ 32 - 1
CHANGELOG.md

@@ -1,9 +1,40 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.0.11...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.1.0...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.1.0](https://github.com/weseek/growi/compare/v5.0.11...v5.1.0) - 2022-07-21
+
+### 💎 Features
+
+- feat: Custom brand logo image (#5709) @mudana-grune
+- feat: Rate Limit by rate-limit-flexible (#6053) @yukendev
+- feat: Audit Log (#5915) @miya
+
+### 🚀 Improvement
+
+- imprv: Prevent XSS with React (#6274) @yuki-takei
+- imprv: Reflect tmp tag data (#6124) @kaoritokashiki
+- imprv: Update subscribe button icon on Navbar (#6213) @jam411
+- imprv: Event emittion by socket.io is triggered only when ES reindexing (#6077) @hirokei-camel
+
+### 🐛 Bug Fixes
+
+- fix: Drawio rendering (#6275) @hakumizuki
+- fix: Blink section header on init (#6249) @yuki-takei
+- fix: Error when trying login with an email that contains plus sign (#6232) @miya
+- fix: Use APIv3 for api get check_username (#6226) @kaoritokashiki
+- fix: Slack integration connection test (#6201) @yukendev
+- fix: Not found page for `/${ObjectId like string}` path (#6208) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Refactor PageInfo types (#6283) @yuki-takei
+- support: Refactor growi renderer using hooks 2 (#6237) @yuki-takei
+- support: Refactor growi renderer using hooks (#6223) @hakumizuki
+- imprv: Omit Personal Container (#6182) @kaoritokashiki
+
 ## [v5.0.11](https://github.com/weseek/growi/compare/v5.0.10...v5.0.11) - 2022-07-05
 
 ### 💎 Features

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 2 - 2
packages/app/docker/README.md

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.1.0`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
-* [`5.1.0-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
+* [`5.1.0`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/docker/Dockerfile)
+* [`5.1.0-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/docker/Dockerfile)
 * [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -63,11 +63,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.0-RC.2",
-    "@growi/plugin-attachment-refs": "^5.1.0-RC.2",
-    "@growi/plugin-lsx": "^5.1.0-RC.2",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.0-RC.2",
-    "@growi/slack": "^5.1.0-RC.2",
+    "@growi/codemirror-textlint": "^5.1.1-RC.0",
+    "@growi/plugin-attachment-refs": "^5.1.1-RC.0",
+    "@growi/plugin-lsx": "^5.1.1-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.1-RC.0",
+    "@growi/slack": "^5.1.1-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -182,7 +182,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.0-RC.2",
+    "@growi/ui": "^5.1.1-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

+ 7 - 1
packages/app/public/static/locales/en_US/admin/admin.json

@@ -207,7 +207,13 @@
     "ctrl_space": "Ctrl+Space to autocomplete",
     "custom_script": "Custom script",
     "write_java": "You can write Javascript that is applied to whole system.",
-    "reflect_change": "You need to reload the page to reflect the change."
+    "reflect_change": "You need to reload the page to reflect the change.",
+    "custom_logo" : "Custom Logo",
+    "default_logo": "Default Logo",
+    "upload_logo": "Upload Logo",
+    "current_logo": "Current Logo",
+    "upload_new_logo": "Upload New Logo",
+    "delete_logo": "Delete Logo"
   },
   "importer_management": {
     "beta_warning": "This function is Beta.",

+ 6 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -1090,6 +1090,12 @@
     "belonging_to_no_group": "Could not find the groups you belong to.",
     "manage_user_groups": "Manage user groups"
   },
+  "crop_image_modal": {
+    "image_crop": "Image Crop",
+    "crop": "Crop",
+    "reset": "Reset",
+    "cancel": "Cancel"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "The list of selectable permissions could not be found. Please modify the permissions on the parent page first and try again.",

+ 7 - 1
packages/app/public/static/locales/ja_JP/admin/admin.json

@@ -207,7 +207,13 @@
     "ctrl_space": "Ctrl+Space でコード補完",
     "custom_script": "カスタムスクリプト",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
-    "reflect_change": "変更の反映はページの更新が必要です。"
+    "reflect_change": "変更の反映はページの更新が必要です。",
+    "custom_logo": "カスタムロゴ",
+    "default_logo": "デフォルトのロゴ",
+    "upload_logo": "ロゴをアップロード",
+    "current_logo": "現在のロゴ",
+    "upload_new_logo": "新しいロゴをアップロードする",
+    "delete_logo": "ロゴを削除"
   },
   "export_management": {
     "exporting_collection_list": "エクスポート中のコレクション",

+ 6 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -1083,6 +1083,12 @@
     "belonging_to_no_group": "所属しているグループが見つかりませんでした。",
     "manage_user_groups": "グループ管理"
   },
+  "crop_image_modal": {
+    "image_crop": "画像の切り抜き",
+    "crop": "トリミング",
+    "reset": "リセット",
+    "cancel": "キャンセル"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "選択可能な権限のリストが見つかりませんでした。まず親ページの権限を修正したのちに再試行してください。",

+ 7 - 1
packages/app/public/static/locales/zh_CN/admin/admin.json

@@ -217,7 +217,13 @@
     "ctrl_space": "Ctrl+Space 自动完成",
     "custom_script": "定制纸条",
     "write_java": "您可以编写应用于整个系统的Javascript。",
-    "reflect_change": "您需要重新加载页面以反映更改。"
+    "reflect_change": "您需要重新加载页面以反映更改。",
+    "custom_logo": "自定义徽标",
+    "default_logo": "默认徽标",
+    "upload_logo": "上传徽标",
+    "current_logo": "当前标志",
+    "upload_new_logo": "上传新徽标",
+    "delete_logo": "删除徽标"
   },
   "importer_management": {
     "beta_warning": "这个函数是Beta。",

+ 6 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -1093,6 +1093,12 @@
     "belonging_to_no_group": "无法找到你所属的团体。",
     "manage_user_groups": "管理用户组"
   },
+  "crop_image_modal": {
+    "image_crop": "图像裁剪",
+    "crop": "修剪",
+    "reset": "重启",
+    "cancel": "取消"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "无法找到可选择的权限列表。 请先修改父页的权限,然后再试一次。",

+ 4 - 0
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -15,6 +15,7 @@ import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
+import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
@@ -76,6 +77,9 @@ function Customize(props) {
         <CustomizeScriptSetting />
       </div>
     */}
+      <div className="mb-5">
+        <CustomizeLogoSetting />
+      </div>
     </div>
   );
 }

+ 185 - 0
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -0,0 +1,185 @@
+import React, { useCallback, useEffect, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastError, toastSuccess } from '~/client/util/apiNotification';
+import {
+  apiv3Delete, apiv3Get, apiv3PostForm, apiv3Put,
+} from '~/client/util/apiv3-client';
+import ImageCropModal from '~/components/Common/ImageCropModal';
+
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const DEFAULT_LOGO = '/images/logo.svg';
+
+const CustomizeLogoSetting = (): JSX.Element => {
+
+  const { t } = useTranslation();
+
+  const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
+  const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
+  const [isDefaultLogo, setIsDefaultLogo] = useState<boolean>(true);
+  const [retrieveError, setRetrieveError] = useState<string | null>(null);
+  const [customizedLogoSrc, setCustomizedLogoSrc] = useState< string | null >(null);
+
+  const retrieveData = useCallback(async() => {
+    try {
+      const response = await apiv3Get('/customize-setting/customize-logo');
+      const { isDefaultLogo: _isDefaultLogo, customizedLogoSrc } = response.data;
+      const isDefaultLogo = _isDefaultLogo ?? true;
+
+      setIsDefaultLogo(isDefaultLogo);
+      setCustomizedLogoSrc(customizedLogoSrc);
+    }
+    catch (err) {
+      setRetrieveError(err);
+      throw new Error('Failed to fetch data');
+    }
+  }, []);
+
+  useEffect(() => {
+    retrieveData();
+  }, [retrieveData]);
+
+  const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files != null && e.target.files.length > 0) {
+      const reader = new FileReader();
+      reader.addEventListener('load', () => setUploadLogoSrc(reader.result));
+      reader.readAsDataURL(e.target.files[0]);
+      setIsImageCropModalShow(true);
+    }
+  }, []);
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      const response = await apiv3Put('/customize-setting/customize-logo', {
+        isDefaultLogo,
+        customizedLogoSrc,
+      });
+      const { customizedParams } = response.data;
+      setIsDefaultLogo(customizedParams.isDefaultLogo);
+      setCustomizedLogoSrc(customizedParams.customizedLogoSrc);
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_logo') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, isDefaultLogo, customizedLogoSrc]);
+
+
+  const onClickDeleteBtn = useCallback(async() => {
+    try {
+      await apiv3Delete('/customize-setting/delete-brand-logo');
+      setCustomizedLogoSrc(null);
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
+    }
+    catch (err) {
+      toastError(err);
+      setRetrieveError(err);
+      throw new Error('Failed to delete logo');
+    }
+  }, [t]);
+
+  const onCropCompleted = useCallback(async(croppedImage) => {
+    try {
+      const formData = new FormData();
+      formData.append('file', croppedImage);
+      const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+      setCustomizedLogoSrc(data.attachment.filePathProxied);
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
+    }
+    catch (err) {
+      toastError(err);
+      setRetrieveError(err);
+      throw new Error('Failed to upload brand logo');
+    }
+    setIsImageCropModalShow(false);
+  }, [t]);
+
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <div className="mb-5">
+            <h2 className="border-bottom my-4 admin-setting-header">{t('admin:customize_setting.custom_logo')}</h2>
+            <div className="row">
+              <div className="col-md-6 col-12 mb-3 mb-md-0">
+                <h4>
+                  <div className="custom-control custom-radio radio-primary">
+                    <input
+                      type="radio"
+                      id="radioDefaultLogo"
+                      className="custom-control-input"
+                      form="formImageType"
+                      name="imagetypeForm[isDefaultLogo]"
+                      checked={isDefaultLogo}
+                      onChange={() => { setIsDefaultLogo(true) }}
+                    />
+                    <label className="custom-control-label" htmlFor="radioDefaultLogo">
+                      {t('admin:customize_setting.default_logo')}
+                    </label>
+                  </div>
+                </h4>
+                <img src={DEFAULT_LOGO} width="64" />
+              </div>
+              <div className="col-md-6 col-12">
+                <h4>
+                  <div className="custom-control custom-radio radio-primary">
+                    <input
+                      type="radio"
+                      id="radioUploadLogo"
+                      className="custom-control-input"
+                      form="formImageType"
+                      name="imagetypeForm[isDefaultLogo]"
+                      checked={!isDefaultLogo}
+                      onChange={() => { setIsDefaultLogo(false) }}
+                    />
+                    <label className="custom-control-label" htmlFor="radioUploadLogo">
+                      { t('admin:customize_setting.upload_logo') }
+                    </label>
+                  </div>
+                </h4>
+                <div className="row mb-3">
+                  <label className="col-sm-4 col-12 col-form-label text-left">
+                    { t('admin:customize_setting.current_logo') }
+                  </label>
+                  <div className="col-sm-8 col-12">
+                    <p><img src={customizedLogoSrc || DEFAULT_LOGO} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>
+                    {(customizedLogoSrc != null) && (
+                      <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                        { t('admin:customize_setting.delete_logo') }
+                      </button>
+                    )}
+                  </div>
+                </div>
+                <div className="row">
+                  <label className="col-sm-4 col-12 col-form-label text-left">
+                    { t('admin:customize_setting.upload_new_logo') }
+                  </label>
+                  <div className="col-sm-8 col-12">
+                    <input type="file" onChange={onSelectFile} name="brandLogo" accept="image/*" />
+                  </div>
+                </div>
+              </div>
+            </div>
+            <AdminUpdateButtonRow onClick={onClickSubmit} disabled={retrieveError != null} />
+          </div>
+        </div>
+      </div>
+
+      <ImageCropModal
+        isShow={isImageCropModalShow}
+        src={uploadLogoSrc}
+        onModalClose={() => setIsImageCropModalShow(false)}
+        onCropCompleted={onCropCompleted}
+        isCircular={false}
+      />
+    </React.Fragment>
+  );
+
+
+};
+
+
+export default CustomizeLogoSetting;

+ 4 - 2
packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -13,13 +13,15 @@ import { apiPost } from '~/client/util/apiv1-client';
 
 
 const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
+  'pages', 'revisions', 'tags', 'pagetagrelations', 'pageredirects', 'comments', 'sharelinks',
 ];
 const GROUPS_USER = [
   'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
+  'useruisettings', 'editorsettings', 'bookmarks', 'subscriptions',
+  'inappnotificationsettings',
 ];
 const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
+  'configs', 'updateposts', 'globalnotificationsettings', 'slackappintegrations',
 ];
 const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 

+ 1 - 1
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -15,7 +15,7 @@ import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 
 
 const IGNORED_COLLECTION_NAMES = [
-  'sessions',
+  'sessions', 'rlflx', 'activities',
 ];
 
 class ExportArchiveDataPage extends React.Component {

+ 127 - 0
packages/app/src/components/Common/ImageCropModal.tsx

@@ -0,0 +1,127 @@
+import React, {
+  FC, useCallback, useEffect, useState,
+} from 'react';
+
+import canvasToBlob from 'async-canvas-to-blob';
+import { useTranslation } from 'react-i18next';
+import ReactCrop from 'react-image-crop';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+} from 'reactstrap';
+
+import { toastError } from '~/client/util/apiNotification';
+import loggerFactory from '~/utils/logger';
+import 'react-image-crop/dist/ReactCrop.css';
+
+const logger = loggerFactory('growi:ImageCropModal');
+
+interface ICropOptions {
+  aspect: number
+  unit: string,
+  x: number
+  y: number
+  width: number,
+  height: number,
+}
+
+type CropOptions = ICropOptions | null
+
+type Props = {
+  isShow: boolean,
+  src: string | ArrayBuffer | null,
+  onModalClose: () => void,
+  onCropCompleted: (res: any) => void,
+  isCircular: boolean,
+}
+const ImageCropModal: FC<Props> = (props: Props) => {
+
+  const {
+    isShow, src, onModalClose, onCropCompleted, isCircular,
+  } = props;
+
+  const [imageRef, setImageRef] = useState<HTMLImageElement>();
+  const [cropOptions, setCropOtions] = useState<CropOptions>(null);
+  const { t } = useTranslation();
+  const reset = useCallback(() => {
+    if (imageRef) {
+      const size = Math.min(imageRef.width, imageRef.height);
+      setCropOtions({
+        aspect: 1,
+        unit: 'px',
+        x: imageRef.width / 2 - size / 2,
+        y: imageRef.height / 2 - size / 2,
+        width: size,
+        height: size,
+      });
+    }
+  }, [imageRef]);
+
+  useEffect(() => {
+    document.body.style.position = 'static';
+    reset();
+  }, [reset]);
+
+  const onImageLoaded = (image) => {
+    setImageRef(image);
+    reset();
+    return false;
+  };
+
+
+  const onCropChange = (crop) => {
+    setCropOtions(crop);
+  };
+
+  const getCroppedImg = async(image, crop) => {
+    const canvas = document.createElement('canvas');
+    const scaleX = image.naturalWidth / image.width;
+    const scaleY = image.naturalHeight / image.height;
+    canvas.width = crop.width;
+    canvas.height = crop.height;
+    const ctx = canvas.getContext('2d');
+    ctx?.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
+    try {
+      const blob = await canvasToBlob(canvas);
+      return blob;
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to draw image'));
+    }
+  };
+
+  const crop = async() => {
+    // crop immages
+    if (imageRef && cropOptions?.width && cropOptions.height) {
+      const result = await getCroppedImg(imageRef, cropOptions);
+      onCropCompleted(result);
+    }
+  };
+
+  return (
+    <Modal isOpen={isShow} toggle={onModalClose}>
+      <ModalHeader tag="h4" toggle={onModalClose} className="bg-info text-light">
+        {t('crop_image_modal.image_crop')}
+      </ModalHeader>
+      <ModalBody className="my-4">
+        <ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />
+      </ModalBody>
+      <ModalFooter>
+        <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={reset}>
+          {t('crop_image_modal.reset')}
+        </button>
+        <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalClose}>
+          {t('crop_image_modal.cancel')}
+        </button>
+        <button type="button" className="btn btn-outline-primary rounded-pill" onClick={crop}>
+          {t('crop_image_modal.crop')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+export default ImageCropModal;

+ 0 - 125
packages/app/src/components/Me/ImageCropModal.jsx

@@ -1,125 +0,0 @@
-import React from 'react';
-
-import canvasToBlob from 'async-canvas-to-blob';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-import ReactCrop from 'react-image-crop';
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-  ModalFooter,
-} from 'reactstrap';
-
-import 'react-image-crop/dist/ReactCrop.css';
-import { toastError } from '~/client/util/apiNotification';
-import loggerFactory from '~/utils/logger';
-
-
-const logger = loggerFactory('growi:ImageCropModal');
-
-class ImageCropModal extends React.Component {
-
-  // demo: https://codesandbox.io/s/72py4jlll6
-  constructor(props) {
-    super();
-    this.state = {
-      crop: null,
-      imageRef: null,
-    };
-    this.onImageLoaded = this.onImageLoaded.bind(this);
-    this.onCropChange = this.onCropChange.bind(this);
-    this.getCroppedImg = this.getCroppedImg.bind(this);
-    this.crop = this.crop.bind(this);
-    this.reset = this.reset.bind(this);
-    this.imageRef = null;
-  }
-
-  onImageLoaded(image) {
-    this.setState({ imageRef: image }, () => this.reset());
-    return false; // Return false when setting crop state in here.
-  }
-
-  onCropChange(crop) {
-    this.setState({ crop });
-  }
-
-  async getCroppedImg(image, crop, fileName) {
-    const canvas = document.createElement('canvas');
-    const scaleX = image.naturalWidth / image.width;
-    const scaleY = image.naturalHeight / image.height;
-    canvas.width = crop.width;
-    canvas.height = crop.height;
-    const ctx = canvas.getContext('2d');
-    ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
-    try {
-      const blob = await canvasToBlob(canvas);
-      return blob;
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(new Error('Failed to draw image'));
-    }
-  }
-
-  async crop() {
-    // crop immages
-    if (this.state.imageRef && this.state.crop.width && this.state.crop.height) {
-      const croppedImage = await this.getCroppedImg(this.state.imageRef, this.state.crop, '/images/icons/user');
-      this.props.onCropCompleted(croppedImage);
-    }
-  }
-
-  reset() {
-    const size = Math.min(this.state.imageRef.width, this.state.imageRef.height);
-    this.setState({
-      crop: {
-        aspect: 1,
-        unit: 'px',
-        x: this.state.imageRef.width / 2 - size / 2,
-        y: this.state.imageRef.height / 2 - size / 2,
-        width: size,
-        height: size,
-      },
-    });
-  }
-
-  render() {
-    return (
-      <Modal isOpen={this.props.show} toggle={this.props.onModalClose}>
-        <ModalHeader tag="h4" toggle={this.props.onModalClose} className="bg-info text-light">
-          Image Crop
-        </ModalHeader>
-        <ModalBody className="my-4">
-          <ReactCrop circularCrop src={this.props.src} crop={this.state.crop} onImageLoaded={this.onImageLoaded} onChange={this.onCropChange} />
-        </ModalBody>
-        <ModalFooter>
-          <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={this.reset}>
-            Reset
-          </button>
-          <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={this.props.onModalClose}>
-            Cancel
-          </button>
-          <button type="button" className="btn btn-outline-primary rounded-pill" onClick={this.crop}>
-            Crop
-          </button>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
-
-ImageCropModal.propTypes = {
-  show: PropTypes.bool.isRequired,
-  src: PropTypes.string,
-  onModalClose: PropTypes.func.isRequired,
-  onCropCompleted: PropTypes.func.isRequired,
-};
-
-const ImageCropModalWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <ImageCropModal t={t} {...props} />;
-};
-
-export default ImageCropModalWrapperFC;

+ 3 - 2
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -6,10 +6,10 @@ import { useTranslation } from 'next-i18next';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost, apiPostForm } from '~/client/util/apiv1-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
+import ImageCropModal from '~/components/Common/ImageCropModal';
 import { useCurrentUser } from '~/stores/context';
 import { generateGravatarSrc, GRAVATAR_DEFAULT } from '~/utils/gravatar';
 
-import ImageCropModal from './ImageCropModal';
 
 const DEFAULT_IMAGE = '/images/icons/user.svg';
 
@@ -155,10 +155,11 @@ const ProfileImageSettings = (): JSX.Element => {
       </div>
 
       <ImageCropModal
-        show={showImageCropModal}
+        isShow={showImageCropModal}
         src={imageCropSrc}
         onModalClose={() => setShowImageCropModal(false)}
         onCropCompleted={cropCompletedHandler}
+        isCircular
       />
 
       <div className="row my-3">

+ 0 - 1
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -121,7 +121,6 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 });
 Confidential.displayName = 'Confidential';
 
-
 export const GrowiNavbar = (): JSX.Element => {
 
   const GlobalSearch = dynamic<GlobalSearchProps>(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });

+ 1 - 1
packages/app/src/interfaces/activity.ts

@@ -138,7 +138,7 @@ const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_GLOBAL_NOTIFICAT
 const ACTION_ADMIN_SLACK_WORKSPACE_CREATE = 'ADMIN_SLACK_WORKSPACE_CREATE';
 const ACTION_ADMIN_SLACK_WORKSPACE_DELETE = 'ADMIN_SLACK_WORKSPACE_DELETE';
 const ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE = 'ADMIN_SLACK_BOT_TYPE_UPDATE';
-const ACTION_ADMIN_SLACK_BOT_TYPE_DELETE = 'ADMIN_SLACK_BOT_TYPE_UPDATE';
+const ACTION_ADMIN_SLACK_BOT_TYPE_DELETE = 'ADMIN_SLACK_BOT_TYPE_DELETE';
 const ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE = 'ADMIN_SLACK_ACCESS_TOKEN_REGENERATE';
 const ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY = 'ADMIN_SLACK_MAKE_APP_PRIMARY';
 const ACTION_ADMIN_SLACK_PERMISSION_UPDATE = 'ADMIN_SLACK_PERMISSION_UPDATE';

+ 44 - 0
packages/app/src/migrations/20220613064207-add-attachment-type-to-existing-attachments.js

@@ -0,0 +1,44 @@
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
+import mongoose from 'mongoose';
+
+import { AttachmentType } from '~/server/interfaces/attachment';
+import attachmentModel from '~/server/models/attachment';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:migrate:add-attachment-type-to-existing-attachments');
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+    const Attachment = getModelSafely('Attachment') || attachmentModel();
+
+    // Add attachmentType for wiki page
+    // Filter pages where "attachmentType" doesn't exist and "page" is not null
+    const operationsForWikiPage = {
+      updateMany:
+         {
+           filter: { page: { $ne: null }, attachmentType: { $exists: false } },
+           update: { $set: { attachmentType: AttachmentType.WIKI_PAGE } },
+         },
+    };
+
+    // Add attachmentType for profile image
+    // Filter pages where "attachmentType" doesn't exist and "page" is null
+    const operationsForProfileImage = {
+      updateMany:
+        {
+          filter: { page: { $eq: null }, attachmentType: { $exists: false } },
+          update: { $set: { attachmentType: AttachmentType.PROFILE_IMAGE } },
+        },
+    };
+    await Attachment.bulkWrite([operationsForWikiPage, operationsForProfileImage]);
+
+    logger.info('Migration has successfully applied');
+
+  },
+
+  async down(db) {
+    // No rollback
+  },
+};

+ 24 - 4
packages/app/src/server/crowi/express-init.js

@@ -1,8 +1,10 @@
 import csrf from 'csurf';
 import mongoose from 'mongoose';
 
+import { i18n, localePath } from '~/next-i18next.config';
+import loggerFactory from '~/utils/logger';
 
-// import { i18n, localePath } from '~/next-i18next.config';
+const logger = loggerFactory('growi:crowi:express-init');
 
 module.exports = function(crowi, app) {
   const debug = require('debug')('growi:crowi:express-init');
@@ -59,11 +61,29 @@ module.exports = function(crowi, app) {
 
   app.use(compression());
 
+
   const { configManager } = crowi;
-  const trustedProxies = configManager.getConfig('crowi', 'security:trustedProxies');
-  if (trustedProxies != null) {
-    app.set('trust proxy', trustedProxies);
+
+  const trustProxyBool = configManager.getConfig('crowi', 'security:trustProxyBool');
+  const trustProxyCsv = configManager.getConfig('crowi', 'security:trustProxyCsv');
+  const trustProxyHops = configManager.getConfig('crowi', 'security:trustProxyHops');
+
+  const trustProxy = trustProxyBool ?? trustProxyCsv ?? trustProxyHops;
+
+  try {
+    if (trustProxy != null) {
+      const isNotSpec = [trustProxyBool, trustProxyCsv, trustProxyHops].filter(trustProxy => trustProxy != null).length !== 1;
+      if (isNotSpec) {
+        // eslint-disable-next-line max-len
+        logger.warn(`If more than one TRUST_PROXY_ ~ environment variable is set, the values are set in the following order of inequality size (BOOL > CSV > HOPS) first. Set value: ${trustProxy}`);
+      }
+      app.set('trust proxy', trustProxy);
+    }
   }
+  catch (err) {
+    logger.error(err);
+  }
+
 
   app.use(helmet({
     contentSecurityPolicy: false,

+ 7 - 0
packages/app/src/server/interfaces/attachment.ts

@@ -0,0 +1,7 @@
+export const AttachmentType = {
+  BRAND_LOGO: 'BRAND_LOGO',
+  WIKI_PAGE: 'WIKI_PAGE',
+  PROFILE_IMAGE: 'PROFILE_IMAGE',
+} as const;
+
+export type AttachmentType = typeof AttachmentType[keyof typeof AttachmentType];

+ 9 - 2
packages/app/src/server/models/attachment.js

@@ -1,5 +1,7 @@
 import loggerFactory from '~/utils/logger';
 
+import { AttachmentType } from '../interfaces/attachment';
+
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
 
@@ -34,6 +36,11 @@ module.exports = function(crowi) {
     fileSize: { type: Number, default: 0 },
     temporaryUrlCached: { type: String },
     temporaryUrlExpiredAt: { type: Date },
+    attachmentType: {
+      type: String,
+      enum: AttachmentType,
+      required: true,
+    },
   }, {
     timestamps: { createdAt: true, updatedAt: false },
   });
@@ -52,7 +59,7 @@ module.exports = function(crowi) {
   attachmentSchema.set('toJSON', { virtuals: true });
 
 
-  attachmentSchema.statics.createWithoutSave = function(pageId, user, fileStream, originalName, fileFormat, fileSize) {
+  attachmentSchema.statics.createWithoutSave = function(pageId, user, fileStream, originalName, fileFormat, fileSize, attachmentType) {
     const Attachment = this;
 
     const extname = path.extname(originalName);
@@ -68,7 +75,7 @@ module.exports = function(crowi) {
     attachment.fileName = fileName;
     attachment.fileFormat = fileFormat;
     attachment.fileSize = fileSize;
-
+    attachment.attachmentType = attachmentType;
     return attachment;
   };
 

+ 5 - 0
packages/app/src/server/models/config.ts

@@ -193,6 +193,8 @@ export const defaultNotificationConfigs: { [key: string]: any } = {
 schema.statics.getLocalconfig = function(crowi) {
   const env = process.env;
 
+  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
+
   const localConfig = {
     crowi: {
       title: crowi.appService.getAppTitle(),
@@ -247,6 +249,9 @@ schema.statics.getLocalconfig = function(crowi) {
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
+    customizedLogoSrc: isDefaultLogo != null && !isDefaultLogo
+      ? crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc')
+      : null,
     auditLogEnabled: crowi.configManager.getConfig('crowi', 'app:auditLogEnabled'),
     activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
     auditLogAvailableActions: crowi.activityService.getAvailableActions(false),

+ 106 - 3
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -1,5 +1,7 @@
 /* eslint-disable no-unused-vars */
+
 import { SupportedAction } from '~/interfaces/activity';
+import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -13,9 +15,11 @@ const express = require('express');
 const router = express.Router();
 
 const { body, query } = require('express-validator');
+const multer = require('multer');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
+
 /**
  * @swagger
  *  tags:
@@ -99,8 +103,9 @@ module.exports = (crowi) => {
 
   const activityEvent = crowi.event('activity');
 
-  const { customizeService } = crowi;
-
+  const { customizeService, attachmentService } = crowi;
+  const Attachment = crowi.model('Attachment');
+  const uploads = multer({ dest: `${crowi.tmpDir}uploads` });
   const validator = {
     layout: [
       body('isContainerFluid').isBoolean(),
@@ -145,6 +150,10 @@ module.exports = (crowi) => {
     customizeScript: [
       body('customizeScript').isString(),
     ],
+    logo: [
+      body('isDefaultLogo').isBoolean().optional({ nullable: true }),
+      body('customizedLogoSrc').isString().optional({ nullable: true }),
+    ],
   };
 
   /**
@@ -168,7 +177,6 @@ module.exports = (crowi) => {
    *                      description: customize params
    */
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
-
     const customizeParams = {
       themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledTimeline: await crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
@@ -675,5 +683,100 @@ module.exports = (crowi) => {
     }
   });
 
+  router.get('/customize-logo', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const isDefaultLogo = await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
+    const customizedLogoSrc = await crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc');
+    return res.apiv3({ isDefaultLogo, customizedLogoSrc });
+  });
+
+  router.put('/customize-logo', csrf, loginRequiredStrictly, adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
+
+    const {
+      isDefaultLogo, customizedLogoSrc,
+    } = req.body;
+
+    const requestParams = {
+      'customize:isDefaultLogo': isDefaultLogo,
+      'customize:customizedLogoSrc': customizedLogoSrc,
+    };
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      const customizedParams = {
+        isDefaultLogo: await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo'),
+        customizedLogoSrc: await crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
+      };
+      return res.apiv3({ customizedParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating customizeLogo';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-customizeLogo-failed'));
+    }
+  });
+
+  router.post('/upload-brand-logo', uploads.single('file'), loginRequiredStrictly,
+    adminRequired, csrf, validator.logo, apiV3FormValidator, async(req, res) => {
+
+      if (req.file == null) {
+        return res.apiv3Err(new ErrorV3('File error.', 'upload-brand-logo-failed'));
+      }
+      if (req.user == null) {
+        return res.apiv3Err(new ErrorV3('param "user" must be set.', 'upload-brand-logo-failed'));
+      }
+
+      const file = req.file;
+
+      // check type
+      const acceptableFileType = /image\/.+/;
+      if (!file.mimetype.match(acceptableFileType)) {
+        const msg = 'File type error. Only image files is allowed to set as user picture.';
+        return res.apiv3Err(new ErrorV3(msg, 'upload-brand-logo-failed'));
+      }
+
+      // Check if previous attachment exists and remove it
+      const attachments = await Attachment.find({ attachmentType: AttachmentType.BRAND_LOGO });
+      if (attachments != null) {
+        await attachmentService.removeAllAttachments(attachments);
+      }
+
+      let attachment;
+      try {
+        attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.BRAND_LOGO);
+        const attachmentConfigParams = {
+          'customize:customizedLogoSrc': attachment.filePathProxied,
+        };
+
+        await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3(err.message, 'upload-brand-logo-failed'));
+      }
+      attachment.toObject({ virtuals: true });
+      return res.apiv3({ attachment });
+    });
+
+  router.delete('/delete-brand-logo', csrf, loginRequiredStrictly, adminRequired, async(req, res) => {
+
+    const attachments = await Attachment.find({ attachmentType: AttachmentType.BRAND_LOGO });
+
+    if (attachments == null) {
+      return res.apiv3Err(new ErrorV3('attachment not found', 'delete-brand-logo-failed'));
+    }
+
+    try {
+      await attachmentService.removeAllAttachments(attachments);
+      // update attachmentId immediately
+      const attachmentConfigParams = { 'customize:customizedLogoSrc': null };
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.status(500).apiv3Err(new ErrorV3('Error while deleting logo', 'delete-brand-logo-failed'));
+    }
+
+    return res.apiv3({});
+  });
+
   return router;
 };

+ 6 - 5
packages/app/src/server/routes/attachment.js

@@ -1,7 +1,8 @@
+
 import { SupportedAction } from '~/interfaces/activity';
+import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
 
-
 /* eslint-disable no-use-before-define */
 
 
@@ -447,7 +448,7 @@ module.exports = function(crowi, app) {
     if (pageId == null && pagePath == null) {
       return res.json(ApiResponse.error('Either page_id or path is required.'));
     }
-    if (!req.file) {
+    if (req.file == null) {
       return res.json(ApiResponse.error('File error.'));
     }
 
@@ -473,7 +474,7 @@ module.exports = function(crowi, app) {
 
     let attachment;
     try {
-      attachment = await attachmentService.createAttachment(file, req.user, pageId);
+      attachment = await attachmentService.createAttachment(file, req.user, pageId, AttachmentType.WIKI_PAGE);
     }
     catch (err) {
       logger.error(err);
@@ -564,7 +565,7 @@ module.exports = function(crowi, app) {
    */
   api.uploadProfileImage = async function(req, res) {
     // check params
-    if (!req.file) {
+    if (req.file == null) {
       return res.json(ApiResponse.error('File error.'));
     }
     if (!req.user) {
@@ -582,7 +583,7 @@ module.exports = function(crowi, app) {
     let attachment;
     try {
       req.user.deleteImage();
-      attachment = await attachmentService.createAttachment(file, req.user);
+      attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.PROFILE_IMAGE);
       await req.user.updateImage(attachment);
     }
     catch (err) {

+ 20 - 65
packages/app/src/server/routes/login-passport.js

@@ -20,7 +20,7 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  const loginSuccessHandler = async(req, res, user) => {
+  const loginSuccessHandler = async(req, res, user, action) => {
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
       if (err) {
@@ -33,6 +33,17 @@ module.exports = function(crowi, app) {
     // remove session.redirectTo
     delete req.session.redirectTo;
 
+    const parameters = {
+      ip:  req.ip,
+      endpoint: req.originalUrl,
+      action,
+      user: req.user?._id,
+      snapshot: {
+        username: req.user.username,
+      },
+    };
+    await crowi.activityService.createActivity(parameters);
+
     return res.safeRedirect(redirectTo);
   };
 
@@ -141,10 +152,7 @@ module.exports = function(crowi, app) {
     await req.logIn(user, (err) => {
       if (err) { debug(err.message); return next() }
 
-      const parameters = { action: SupportedAction.ACTION_USER_LOGIN_WITH_LDAP };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return loginSuccessHandler(req, res, user);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LDAP);
     });
   };
 
@@ -236,10 +244,7 @@ module.exports = function(crowi, app) {
       req.logIn(user, (err) => {
         if (err) { debug(err.message); return next() }
 
-        const parameters = { action: SupportedAction.ACTION_USER_LOGIN_WITH_LOCAL };
-        activityEvent.emit('update', res.locals.activity._id, parameters);
-
-        return loginSuccessHandler(req, res, user);
+        return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LOCAL);
       });
     })(req, res, next);
   };
@@ -310,18 +315,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
 
-      const parameters = {
-        ip:  req.ip,
-        endpoint: req.originalUrl,
-        action: SupportedAction.ACTION_USER_LOGIN_WITH_GOOGLE,
-        user: req.user?._id,
-        snapshot: {
-          username: req.user?.username,
-        },
-      };
-      await crowi.activityService.createActivity(parameters);
-
-      return loginSuccessHandler(req, res, user);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GOOGLE);
     });
   };
 
@@ -364,18 +358,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
 
-      const parameters = {
-        ip:  req.ip,
-        endpoint: req.originalUrl,
-        action: SupportedAction.ACTION_USER_LOGIN_WITH_GITHUB,
-        user: req.user?._id,
-        snapshot: {
-          username: req.user?.username,
-        },
-      };
-      await crowi.activityService.createActivity(parameters);
-
-      return loginSuccessHandler(req, res, user);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GITHUB);
     });
   };
 
@@ -418,18 +401,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
 
-      const parameters = {
-        ip:  req.ip,
-        endpoint: req.originalUrl,
-        action: SupportedAction.ACTION_USER_LOGIN_WITH_TWITTER,
-        user: req.user?._id,
-        snapshot: {
-          username: req.user?.username,
-        },
-      };
-      await crowi.activityService.createActivity(parameters);
-
-      return loginSuccessHandler(req, res, user);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_TWITTER);
     });
   };
 
@@ -478,18 +450,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
 
-      const parameters = {
-        ip:  req.ip,
-        endpoint: req.originalUrl,
-        action: SupportedAction.ACTION_USER_LOGIN_WITH_OIDC,
-        user: req.user?._id,
-        snapshot: {
-          username: req.user?.username,
-        },
-      };
-      await crowi.activityService.createActivity(parameters);
-
-      return loginSuccessHandler(req, res, user);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_OIDC);
     });
   };
 
@@ -552,10 +513,7 @@ module.exports = function(crowi, app) {
         return loginFailureHandler(req, res);
       }
 
-      const parameters = { action: SupportedAction.ACTION_USER_LOGIN_WITH_SAML };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return loginSuccessHandler(req, res, user);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_SAML);
     });
   };
 
@@ -598,10 +556,7 @@ module.exports = function(crowi, app) {
     await req.logIn(user, (err) => {
       if (err) { debug(err.message); return next() }
 
-      const parameters = { action: SupportedAction.ACTION_USER_LOGIN_WITH_BASIC };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return loginSuccessHandler(req, res, user);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_BASIC);
     });
   };
 

+ 4 - 3
packages/app/src/server/service/attachment.js

@@ -1,8 +1,9 @@
 import loggerFactory from '~/utils/logger';
 
-const mongoose = require('mongoose');
 const fs = require('fs');
 
+const mongoose = require('mongoose');
+
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:service:AttachmentService');
 
@@ -15,7 +16,7 @@ class AttachmentService {
     this.crowi = crowi;
   }
 
-  async createAttachment(file, user, pageId = null) {
+  async createAttachment(file, user, pageId = null, attachmentType) {
     const { fileUploadService } = this.crowi;
 
     // check limit
@@ -33,7 +34,7 @@ class AttachmentService {
     // create an Attachment document and upload file
     let attachment;
     try {
-      attachment = Attachment.createWithoutSave(pageId, user, fileStream, file.originalname, file.mimetype, file.size);
+      attachment = Attachment.createWithoutSave(pageId, user, fileStream, file.originalname, file.mimetype, file.size, attachmentType);
       await fileUploadService.uploadFile(fileStream, attachment);
       await attachment.save();
     }

+ 14 - 2
packages/app/src/server/service/config-loader.ts

@@ -358,12 +358,24 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.BOOLEAN,
     default: false,
   },
-  TRUSTED_PROXIES: {
+  TRUST_PROXY_BOOL: {
     ns:      'crowi',
-    key:     'security:trustedProxies',
+    key:     'security:trustProxyBool',
+    type:    ValueType.BOOLEAN,
+    default: null,
+  },
+  TRUST_PROXY_CSV: {
+    ns:      'crowi',
+    key:     'security:trustProxyCsv',
     type:    ValueType.STRING,
     default: null,
   },
+  TRUST_PROXY_HOPS: {
+    ns:      'crowi',
+    key:     'security:trustProxyHops',
+    type:    ValueType.NUMBER,
+    default: null,
+  },
   LOCAL_STRATEGY_ENABLED: {
     ns:      'crowi',
     key:     'security:passport-local:isEnabled',

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

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

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

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

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

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

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "5.1.0-slackbot-proxy.0",
+  "version": "5.1.1-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.1.0-RC.2",
+    "@growi/slack": "^5.1.1-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

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