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

Merge pull request #5708 from weseek/feat/gw7772-store-and-retrieve-logo-settings

feat: gw7772 store and retrieve logo settings
Mudana-Grune 4 лет назад
Родитель
Сommit
e9b3f10aa2

+ 2 - 1
packages/app/resource/locales/en_US/admin/admin.json

@@ -200,7 +200,8 @@
     "ctrl_space": "Ctrl+Space to autocomplete",
     "ctrl_space": "Ctrl+Space to autocomplete",
     "custom_script": "Custom script",
     "custom_script": "Custom script",
     "write_java": "You can write Javascript that is applied to whole system.",
     "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"
   },
   },
   "importer_management": {
   "importer_management": {
     "beta_warning": "This function is Beta.",
     "beta_warning": "This function is Beta.",

+ 2 - 1
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -200,7 +200,8 @@
     "ctrl_space": "Ctrl+Space でコード補完",
     "ctrl_space": "Ctrl+Space でコード補完",
     "custom_script": "カスタムスクリプト",
     "custom_script": "カスタムスクリプト",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
-    "reflect_change": "変更の反映はページの更新が必要です。"
+    "reflect_change": "変更の反映はページの更新が必要です。",
+    "custom_logo": "カスタムロゴ"
   },
   },
   "export_management": {
   "export_management": {
     "exporting_collection_list": "エクスポート中のコレクション",
     "exporting_collection_list": "エクスポート中のコレクション",

+ 2 - 1
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -210,7 +210,8 @@
     "ctrl_space": "Ctrl+Space 自动完成",
     "ctrl_space": "Ctrl+Space 自动完成",
     "custom_script": "定制纸条",
     "custom_script": "定制纸条",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "write_java": "您可以编写应用于整个系统的Javascript。",
-    "reflect_change": "您需要重新加载页面以反映更改。"
+    "reflect_change": "您需要重新加载页面以反映更改。",
+    "custom_logo": "自定义徽标"
   },
   },
   "importer_management": {
   "importer_management": {
     "beta_warning": "这个函数是Beta。",
     "beta_warning": "这个函数是Beta。",

+ 62 - 13
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -56,16 +56,17 @@ export default class AdminCustomizeContainer extends Container {
         'tomorrow-night':   { name: '[Dark] Tomorrow Night',  border: false },
         'tomorrow-night':   { name: '[Dark] Tomorrow Night',  border: false },
         'vs2015':           { name: '[Dark] Vs 2015',         border: false },
         'vs2015':           { name: '[Dark] Vs 2015',         border: false },
       },
       },
-      uploadedLogoSrc: this.getUploadedLogoSrc(),
+      uploadedLogoSrc: '',
       isUploadedLogo: false,
       isUploadedLogo: false,
       defaultLogoSrc: DEFAULT_LOGO,
       defaultLogoSrc: DEFAULT_LOGO,
+      isDefaultLogo: false,
+      attachmentId: '',
       /* eslint-enable quote-props, no-multi-spaces */
       /* eslint-enable quote-props, no-multi-spaces */
     };
     };
     this.switchPageListLimitationS = this.switchPageListLimitationS.bind(this);
     this.switchPageListLimitationS = this.switchPageListLimitationS.bind(this);
     this.switchPageListLimitationM = this.switchPageListLimitationM.bind(this);
     this.switchPageListLimitationM = this.switchPageListLimitationM.bind(this);
     this.switchPageListLimitationL = this.switchPageListLimitationL.bind(this);
     this.switchPageListLimitationL = this.switchPageListLimitationL.bind(this);
     this.switchPageListLimitationXL = this.switchPageListLimitationXL.bind(this);
     this.switchPageListLimitationXL = this.switchPageListLimitationXL.bind(this);
-    this.getUploadedLogoSrc = this.getUploadedLogoSrc.bind(this);
     this.deleteLogo = this.deleteLogo.bind(this);
     this.deleteLogo = this.deleteLogo.bind(this);
     this.uploadAttachment = this.uploadAttachment.bind(this);
     this.uploadAttachment = this.uploadAttachment.bind(this);
 
 
@@ -104,10 +105,23 @@ export default class AdminCustomizeContainer extends Container {
         currentCustomizeHeader: customizeParams.customizeHeader,
         currentCustomizeHeader: customizeParams.customizeHeader,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeScript: customizeParams.customizeScript,
         currentCustomizeScript: customizeParams.customizeScript,
+        attachmentId: customizeParams.attachmentLogoId,
+        isDefaultLogo: customizeParams.isDefaultLogo,
       });
       });
-
       // search style name from object for display
       // search style name from object for display
       this.setState({ currentHighlightJsStyleName: this.state.highlightJsCssSelectorOptions[customizeParams.styleName].name });
       this.setState({ currentHighlightJsStyleName: this.state.highlightJsCssSelectorOptions[customizeParams.styleName].name });
+
+      // set current uploaded logo
+      if (customizeParams.attachmentLogoId) {
+        const logoPath = `/attachment/${customizeParams.attachmentLogoId}`;
+
+        this.setState({ isUploadedLogo: true });
+        this.setState({ uploadedLogoSrc: logoPath });
+      }
+      else {
+        this.setState({ isUploadedLogo: false });
+        this.setState({ uploadedLogoSrc: DEFAULT_LOGO });
+      }
     }
     }
     catch (err) {
     catch (err) {
       this.setState({ retrieveError: err });
       this.setState({ retrieveError: err });
@@ -436,20 +450,26 @@ export default class AdminCustomizeContainer extends Container {
     }
     }
   }
   }
 
 
-  getUploadedLogoSrc() {
-    this.setState({ isUploadedLogo: false });
-    return DEFAULT_LOGO;
-  }
 
 
   async deleteLogo() {
   async deleteLogo() {
     try {
     try {
-      // await this.appContainer.apiPost('/attachments.removeProfileImage', { _csrf: this.appContainer.csrfToken });
-      this.setState({ isUploadedLogo: false, uploadedLogoSrc: DEFAULT_LOGO });
+      const formData = {
+        _csrf:  this.appContainer.csrfToken,
+        attachmentId: this.state.attachmentId,
+      };
+      await this.appContainer.apiPost('/attachments.removeBrandLogo', formData);
+      this.setState({
+        isUploadedLogo: false,
+        uploadedLogoSrc: DEFAULT_LOGO,
+        attachmentId: null,
+        isDefaultLogo: true,
+      });
+
     }
     }
     catch (err) {
     catch (err) {
       this.setState({ retrieveError: err });
       this.setState({ retrieveError: err });
       logger.error(err);
       logger.error(err);
-      throw new Error('Failed to delete profile image');
+      throw new Error('Failed to delete logo');
     }
     }
   }
   }
 
 
@@ -458,13 +478,42 @@ export default class AdminCustomizeContainer extends Container {
       const formData = new FormData();
       const formData = new FormData();
       formData.append('file', file);
       formData.append('file', file);
       formData.append('_csrf', this.appContainer.csrfToken);
       formData.append('_csrf', this.appContainer.csrfToken);
-      // const response = await this.appContainer.apiPost('/attachments.uploadProfileImage', formData);
-      // this.setState({ isUploadedLogo: true, uploadedLogoSrc: response.attachment.filePathProxied });
+      formData.append('attachmentType', 'BRAND_LOGO');
+      formData.append('attachmentId', this.state.attachmentId);
+      const response = await this.appContainer.apiPost('/attachments.uploadBrandLogo', formData);
+
+      this.setState({
+        isUploadedLogo: true,
+        uploadedLogoSrc: response.attachment.filePathProxied,
+        attachmentId: response.attachment.id,
+      });
     }
     }
     catch (err) {
     catch (err) {
       this.setState({ retrieveError: err });
       this.setState({ retrieveError: err });
       logger.error(err);
       logger.error(err);
-      throw new Error('Failed to upload profile image');
+      throw new Error('Failed to upload brand logo');
+    }
+  }
+
+  switchDefaultLogo() {
+    this.setState({ isDefaultLogo: !this.state.isDefaultLogo });
+  }
+
+  async updateCustomizeLogo() {
+    try {
+      const response = await this.appContainer.apiv3.put('/customize-setting/customize-logo', {
+        isDefaultLogo: this.state.isDefaultLogo,
+        attachmentId: this.state.attachmentId,
+      });
+      const { customizedParams } = response.data;
+      this.setState({
+        isDefaultLogo: customizedParams.isDefaultLogo,
+        attachmentId:  customizedParams.attachmentId,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
     }
     }
   }
   }
 
 

+ 1 - 0
packages/app/src/client/services/PersonalContainer.js

@@ -204,6 +204,7 @@ export default class PersonalContainer extends Container {
       const formData = new FormData();
       const formData = new FormData();
       formData.append('file', file);
       formData.append('file', file);
       formData.append('_csrf', this.appContainer.csrfToken);
       formData.append('_csrf', this.appContainer.csrfToken);
+      formData.append('attachmentType', 'PROFILE_IMAGE');
       const response = await this.appContainer.apiPost('/attachments.uploadProfileImage', formData);
       const response = await this.appContainer.apiPost('/attachments.uploadProfileImage', formData);
       this.setState({ isUploadedPicture: true, uploadedPictureSrc: response.attachment.filePathProxied });
       this.setState({ isUploadedPicture: true, uploadedPictureSrc: response.attachment.filePathProxied });
     }
     }

+ 13 - 7
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.jsx

@@ -19,7 +19,6 @@ class CustomizeLogoSetting extends React.Component {
     this.state = {
     this.state = {
       show: false,
       show: false,
       src: null,
       src: null,
-      croppedImage: null,
     };
     };
 
 
     // this.imageRef = null;
     // this.imageRef = null;
@@ -44,8 +43,8 @@ class CustomizeLogoSetting extends React.Component {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
     try {
     try {
-      await adminCustomizeContainer.uploadAttachment(this.state.croppedImage);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_script') }));
+      await adminCustomizeContainer.updateCustomizeLogo();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_logo') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -64,7 +63,14 @@ class CustomizeLogoSetting extends React.Component {
   }
   }
 
 
   async onCropCompleted(croppedImage) {
   async onCropCompleted(croppedImage) {
-    this.setState({ croppedImage });
+    const { t, adminCustomizeContainer } = this.props;
+    try {
+      await adminCustomizeContainer.uploadAttachment(croppedImage);
+      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
     this.hideModal();
     this.hideModal();
   }
   }
 
 
@@ -87,7 +93,7 @@ class CustomizeLogoSetting extends React.Component {
       <React.Fragment>
       <React.Fragment>
         <div className="row">
         <div className="row">
           <div className="col-12">
           <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_script')}</h2>
+            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_logo')}</h2>
             <div className="row">
             <div className="row">
               <div className="col-md-6 col-12">
               <div className="col-md-6 col-12">
                 <h4>
                 <h4>
@@ -99,7 +105,7 @@ class CustomizeLogoSetting extends React.Component {
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
                       checked={isDefaultLogo}
                       checked={isDefaultLogo}
-                      onChange={() => { adminCustomizeContainer.changeIsDefaultLogoEnabled(true) }}
+                      onChange={() => { adminCustomizeContainer.switchDefaultLogo() }}
                     />
                     />
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                       Default Logo
                       Default Logo
@@ -118,7 +124,7 @@ class CustomizeLogoSetting extends React.Component {
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
                       checked={!isDefaultLogo}
                       checked={!isDefaultLogo}
-                      onChange={() => { adminCustomizeContainer.changeIsDefaultLogoEnabled(false) }}
+                      onChange={() => { adminCustomizeContainer.switchDefaultLogo() }}
                     />
                     />
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                       { t('Upload Logo') }
                       { t('Upload Logo') }

+ 17 - 4
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -14,6 +14,7 @@ import { useIsSearchPage, useCurrentPagePath } from '~/stores/context';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import GrowiLogo from '../Icons/GrowiLogo';
 import GrowiLogo from '../Icons/GrowiLogo';
 
 
+
 import PersonalDropdown from './PersonalDropdown';
 import PersonalDropdown from './PersonalDropdown';
 import GlobalSearch from './GlobalSearch';
 import GlobalSearch from './GlobalSearch';
 import InAppNotificationDropdown from '../InAppNotification/InAppNotificationDropdown';
 import InAppNotificationDropdown from '../InAppNotification/InAppNotificationDropdown';
@@ -86,22 +87,34 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps) => {
   );
   );
 });
 });
 
 
+interface NavbarLogoProps {
+  logoSrc?: string,
+}
+const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
+
+  const { logoSrc } = props;
+  return logoSrc
+    ? (<img src={logoSrc} className="picture picture-lg " id="settingBrandLogo" width="32" />)
+    : <GrowiLogo />;
+
+});
 
 
 const GrowiNavbar = (props) => {
 const GrowiNavbar = (props) => {
 
 
   const { appContainer } = props;
   const { appContainer } = props;
   const { currentUser } = appContainer;
   const { currentUser } = appContainer;
-  const { crowi, isSearchServiceConfigured } = appContainer.config;
-
+  const {
+    crowi, isSearchServiceConfigured, isDefaultLogo, uploadedLogoSrc,
+  } = appContainer.config;
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isSearchPage } = useIsSearchPage();
   const { data: isSearchPage } = useIsSearchPage();
-
+  const logoSrc = !isDefaultLogo ? uploadedLogoSrc : null;
   return (
   return (
     <>
     <>
       {/* Brand Logo  */}
       {/* Brand Logo  */}
       <div className="navbar-brand mr-0">
       <div className="navbar-brand mr-0">
         <a className="grw-logo d-block" href="/">
         <a className="grw-logo d-block" href="/">
-          <GrowiLogo />
+          <GrowiNavbarLogo logoSrc={...logoSrc} />
         </a>
         </a>
       </div>
       </div>
 
 

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

@@ -35,6 +35,11 @@ module.exports = function(crowi) {
     createdAt: { type: Date, default: Date.now },
     createdAt: { type: Date, default: Date.now },
     temporaryUrlCached: { type: String },
     temporaryUrlCached: { type: String },
     temporaryUrlExpiredAt: { type: Date },
     temporaryUrlExpiredAt: { type: Date },
+    attachmentType: {
+      type: String,
+      enum: ['BRAND_LOGO', 'WIKI_PAGE', 'USER_PAGE', 'PROFILE_IMAGE', null],
+      default: null,
+    },
   });
   });
   attachmentSchema.plugin(uniqueValidator);
   attachmentSchema.plugin(uniqueValidator);
   attachmentSchema.plugin(mongoosePaginate);
   attachmentSchema.plugin(mongoosePaginate);
@@ -51,7 +56,7 @@ module.exports = function(crowi) {
   attachmentSchema.set('toJSON', { virtuals: true });
   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 = null) {
     const Attachment = this;
     const Attachment = this;
 
 
     const extname = path.extname(originalName);
     const extname = path.extname(originalName);
@@ -68,7 +73,7 @@ module.exports = function(crowi) {
     attachment.fileFormat = fileFormat;
     attachment.fileFormat = fileFormat;
     attachment.fileSize = fileSize;
     attachment.fileSize = fileSize;
     attachment.createdAt = Date.now();
     attachment.createdAt = Date.now();
-
+    attachment.attachmentType = attachmentType;
     return attachment;
     return attachment;
   };
   };
 
 

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

@@ -135,6 +135,9 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:isEnabledStaleNotification': false,
   'customize:isEnabledStaleNotification': false,
   'customize:isAllReplyShown': false,
   'customize:isAllReplyShown': false,
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isSearchScopeChildrenAsDefault': false,
+  'customize:attachmentLogoId' : undefined,
+  'customize:isDefaultLogo': true,
+  'customize:uploadedLogoSrc': undefined,
 
 
   'notification:owner-page:isEnabled': false,
   'notification:owner-page:isEnabled': false,
   'notification:group-page:isEnabled': false,
   'notification:group-page:isEnabled': false,
@@ -242,6 +245,9 @@ schema.statics.getLocalconfig = function(crowi) {
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
+    isDefaultLogo:  crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo'),
+    attachmentLogoId: crowi.configManager.getConfig('crowi', 'customize:attachmentLogoId'),
+    uploadedLogoSrc: crowi.configManager.getConfig('crowi', 'customize:uploadedLogoSrc'),
   };
   };
 
 
   return localConfig;
   return localConfig;

+ 29 - 0
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -135,6 +135,10 @@ module.exports = (crowi) => {
     customizeScript: [
     customizeScript: [
       body('customizeScript').isString(),
       body('customizeScript').isString(),
     ],
     ],
+    logo: [
+      body('attachmentId').isString().optional({ nullable: true }),
+      body('isDefaultLogo').isBoolean(),
+    ],
   };
   };
 
 
   /**
   /**
@@ -177,6 +181,8 @@ module.exports = (crowi) => {
       customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
       customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
       customizeCss: await crowi.configManager.getConfig('crowi', 'customize:css'),
       customizeCss: await crowi.configManager.getConfig('crowi', 'customize:css'),
       customizeScript: await crowi.configManager.getConfig('crowi', 'customize:script'),
       customizeScript: await crowi.configManager.getConfig('crowi', 'customize:script'),
+      attachmentLogoId: await crowi.configManager.getConfig('crowi', 'customize:attachmentLogoId'),
+      isDefaultLogo: await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo'),
     };
     };
 
 
     return res.apiv3({ customizeParams });
     return res.apiv3({ customizeParams });
@@ -609,5 +615,28 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
+  router.put('/customize-logo', loginRequiredStrictly, adminRequired, csrf, validator.logo, apiV3FormValidator, async(req, res) => {
+
+    // Set default logo when uploaded logo is empty
+    const isDefaultLogo = req.body.attachmentId ? req.body.isDefaultLogo : true;
+    const requestParams = {
+      'customize:attachmentLogoId': req.body.attachmentId,
+      'customize:isDefaultLogo': isDefaultLogo,
+    };
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      const customizedParams = {
+        attachmentLogoId: await crowi.configManager.getConfig('crowi', 'customize:attachmentLogoId'),
+        isDefaultLogo: await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo'),
+      };
+      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'));
+    }
+  });
+
   return router;
   return router;
 };
 };

+ 72 - 3
packages/app/src/server/routes/attachment.js

@@ -1,5 +1,5 @@
+import mongoose from 'mongoose';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-
 /* eslint-disable no-use-before-define */
 /* eslint-disable no-use-before-define */
 
 
 
 
@@ -550,7 +550,7 @@ module.exports = function(crowi, app) {
     }
     }
 
 
     const file = req.file;
     const file = req.file;
-
+    const attachmentType = req.body.attachmentType;
     // check type
     // check type
     const acceptableFileType = /image\/.+/;
     const acceptableFileType = /image\/.+/;
     if (!file.mimetype.match(acceptableFileType)) {
     if (!file.mimetype.match(acceptableFileType)) {
@@ -560,7 +560,7 @@ module.exports = function(crowi, app) {
     let attachment;
     let attachment;
     try {
     try {
       req.user.deleteImage();
       req.user.deleteImage();
-      attachment = await attachmentService.createAttachment(file, req.user);
+      attachment = await attachmentService.createAttachment(file, req.user, null, attachmentType);
       await req.user.updateImage(attachment);
       await req.user.updateImage(attachment);
     }
     }
     catch (err) {
     catch (err) {
@@ -699,5 +699,74 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success({}));
     return res.json(ApiResponse.success({}));
   };
   };
 
 
+  api.uploadBrandLogo = async function(req, res) {
+    // check params
+    if (!req.file) {
+      return res.json(ApiResponse.error('File error.'));
+    }
+    if (!req.user) {
+      return res.json(ApiResponse.error('param "user" must be set.'));
+    }
+
+    const file = req.file;
+    const attachmentType = req.body.attachmentType;
+    const attachmentId = req.body.attachmentId !== 'null' ? mongoose.Types.ObjectId(req.body.attachmentId) : null;
+    // check type
+    const acceptableFileType = /image\/.+/;
+    if (!file.mimetype.match(acceptableFileType)) {
+      return res.json(ApiResponse.error('File type error. Only image files is allowed to set as user picture.'));
+    }
+
+    // remove previous attachment
+    if (attachmentId) {
+      await attachmentService.removeAttachment(attachmentId);
+    }
+
+    let attachment;
+    try {
+      attachment = await attachmentService.createAttachment(file, req.user, null, attachmentType);
+      const attachmentConfigParams = {
+        'customize:attachmentLogoId': attachment.id,
+        'customize:uploadedLogoSrc': attachment.filePathProxied,
+      };
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.json(ApiResponse.error(err.message));
+    }
+
+    const result = {
+      attachment: attachment.toObject({ virtuals: true }),
+    };
+
+    return res.json(ApiResponse.success(result));
+  };
+
+  api.removeBrandLogo = async function(req, res) {
+    const attachmentId = mongoose.Types.ObjectId(req.body.attachmentId);
+    const attachment = await Attachment.findById(attachmentId);
+
+    if (attachment == null) {
+      return res.json(ApiResponse.error('attachment not found'));
+    }
+
+    try {
+      await attachmentService.removeAttachment(attachmentId);
+      // update attachmentLogoId immediately
+      const attachmentConfigParams = {
+        'customize:attachmentLogoId': null,
+        'customize:isDefaultLogo': true,
+      };
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.status(500).json(ApiResponse.error('Error while deleting logo'));
+    }
+
+    return res.json(ApiResponse.success({}));
+  };
+
   return actions;
   return actions;
 };
 };

+ 2 - 0
packages/app/src/server/routes/index.js

@@ -188,6 +188,8 @@ module.exports = function(crowi, app) {
   apiV1Router.post('/attachments.remove'               , accessTokenParser , loginRequiredStrictly , csrf, attachment.api.remove);
   apiV1Router.post('/attachments.remove'               , accessTokenParser , loginRequiredStrictly , csrf, attachment.api.remove);
   apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser , loginRequiredStrictly , csrf, attachment.api.removeProfileImage);
   apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser , loginRequiredStrictly , csrf, attachment.api.removeProfileImage);
   apiV1Router.get('/attachments.limit'   , accessTokenParser , loginRequiredStrictly, attachment.api.limit);
   apiV1Router.get('/attachments.limit'   , accessTokenParser , loginRequiredStrictly, attachment.api.limit);
+  apiV1Router.post('/attachments.uploadBrandLogo'   , uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly ,csrf, attachment.api.uploadBrandLogo);
+  apiV1Router.post('/attachments.removeBrandLogo'      , accessTokenParser , loginRequiredStrictly , csrf, attachment.api.removeBrandLogo);
 
 
   // API v1
   // API v1
   app.use('/_api', unavailableWhenMaintenanceModeForApi, apiV1Router);
   app.use('/_api', unavailableWhenMaintenanceModeForApi, apiV1Router);

+ 2 - 2
packages/app/src/server/service/attachment.js

@@ -15,7 +15,7 @@ class AttachmentService {
     this.crowi = crowi;
     this.crowi = crowi;
   }
   }
 
 
-  async createAttachment(file, user, pageId = null) {
+  async createAttachment(file, user, pageId = null, attachmentType = null) {
     const { fileUploadService } = this.crowi;
     const { fileUploadService } = this.crowi;
 
 
     // check limit
     // check limit
@@ -33,7 +33,7 @@ class AttachmentService {
     // create an Attachment document and upload file
     // create an Attachment document and upload file
     let attachment;
     let attachment;
     try {
     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 fileUploadService.uploadFile(fileStream, attachment);
       await attachment.save();
       await attachment.save();
     }
     }