Sfoglia il codice sorgente

Merge branch 'imprv/profile-image-cache' into imprv/remove-image-population-apiv3

# Conflicts:
#	src/server/models/user.js
yusuketk 6 anni fa
parent
commit
959e9f3792

+ 2 - 2
package.json

@@ -194,8 +194,8 @@
     "load-css-file": "^1.0.0",
     "load-css-file": "^1.0.0",
     "lodash-webpack-plugin": "^0.11.5",
     "lodash-webpack-plugin": "^0.11.5",
     "markdown-it": "^10.0.0",
     "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.0.3",
-    "markdown-it-drawio-viewer": "^1.1.3",
+    "markdown-it-blockdiag": "^1.1.1",
+    "markdown-it-drawio-viewer": "^1.2.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",
     "markdown-it-mathjax": "^2.0.0",

+ 8 - 1
src/client/js/components/Admin/UserManagement.jsx

@@ -121,7 +121,14 @@ class UserManagement extends React.Component {
 
 
     return (
     return (
       <Fragment>
       <Fragment>
-        {adminUsersContainer.state.userForPasswordResetModal && <PasswordResetModal />}
+        {adminUsersContainer.state.userForPasswordResetModal != null
+        && (
+        <PasswordResetModal
+          isOpen={adminUsersContainer.state.isPasswordResetModalShown}
+          onClose={adminUsersContainer.hidePasswordResetModal}
+          userForPasswordResetModal={adminUsersContainer.state.userForPasswordResetModal}
+        />
+        )}
         <p>
         <p>
           <InviteUserControl />
           <InviteUserControl />
           <a className="btn btn-default btn-outline ml-2" href="/admin/users/external-accounts">
           <a className="btn btn-default btn-outline ml-2" href="/admin/users/external-accounts">

+ 15 - 15
src/client/js/components/Admin/Users/PasswordResetModal.jsx

@@ -7,7 +7,6 @@ import Modal from 'react-bootstrap/es/Modal';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
-import AdminUsersContainer from '../../../services/AdminUsersContainer';
 
 
 class PasswordResetModal extends React.Component {
 class PasswordResetModal extends React.Component {
 
 
@@ -23,10 +22,9 @@ class PasswordResetModal extends React.Component {
   }
   }
 
 
   async resetPassword() {
   async resetPassword() {
-    const { appContainer, adminUsersContainer } = this.props;
-    const user = adminUsersContainer.state.userForPasswordResetModal;
+    const { appContainer, userForPasswordResetModal } = this.props;
 
 
-    const res = await appContainer.apiPost('/admin/users.resetPassword', { user_id: user._id });
+    const res = await appContainer.apiPost('/admin/users.resetPassword', { user_id: userForPasswordResetModal._id });
     if (res.ok) {
     if (res.ok) {
       this.setState({ temporaryPassword: res.newPassword, isPasswordResetDone: true });
       this.setState({ temporaryPassword: res.newPassword, isPasswordResetDone: true });
     }
     }
@@ -36,14 +34,13 @@ class PasswordResetModal extends React.Component {
   }
   }
 
 
   renderModalBodyBeforeReset() {
   renderModalBodyBeforeReset() {
-    const { t, adminUsersContainer } = this.props;
-    const user = adminUsersContainer.state.userForPasswordResetModal;
+    const { t, userForPasswordResetModal } = this.props;
 
 
     return (
     return (
       <div>
       <div>
         <p className="alert alert-danger">{t('admin:user_management.reset_password_modal.password_reset_message')}</p>
         <p className="alert alert-danger">{t('admin:user_management.reset_password_modal.password_reset_message')}</p>
         <p>
         <p>
-          {t('admin:user_management.reset_password_modal.target_user')}: <code>{user.email}</code>
+          {t('admin:user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
         </p>
         </p>
         <p>
         <p>
           {t('admin:user_management.reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
           {t('admin:user_management.reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
@@ -53,8 +50,7 @@ class PasswordResetModal extends React.Component {
   }
   }
 
 
   returnModalBodyAfterReset() {
   returnModalBodyAfterReset() {
-    const { t, adminUsersContainer } = this.props;
-    const user = adminUsersContainer.state.userForPasswordResetModal;
+    const { t, userForPasswordResetModal } = this.props;
 
 
     return (
     return (
       <div>
       <div>
@@ -63,7 +59,7 @@ class PasswordResetModal extends React.Component {
           <span className="text-danger">{t('admin:user_management.reset_password_modal.send_new_password')}</span>
           <span className="text-danger">{t('admin:user_management.reset_password_modal.send_new_password')}</span>
         </p>
         </p>
         <p>
         <p>
-          {t('admin:user_management.reset_password_modal.target_user')}: <code>{user.email}</code>
+          {t('admin:user_management.reset_password_modal.target_user')}: <code>{userForPasswordResetModal.email}</code>
         </p>
         </p>
         <button type="submit" className="btn btn-primary" onClick={this.resetPassword}>
         <button type="submit" className="btn btn-primary" onClick={this.resetPassword}>
           {t('admin:user_management.reset_password')}
           {t('admin:user_management.reset_password')}
@@ -75,17 +71,17 @@ class PasswordResetModal extends React.Component {
   returnModalFooter() {
   returnModalFooter() {
     return (
     return (
       <div>
       <div>
-        <button type="submit" className="btn btn-primary" onClick={this.props.adminUsersContainer.hidePasswordResetModal}>OK</button>
+        <button type="submit" className="btn btn-primary" onClick={this.props.onClose}>OK</button>
       </div>
       </div>
     );
     );
   }
   }
 
 
 
 
   render() {
   render() {
-    const { t, adminUsersContainer } = this.props;
+    const { t } = this.props;
 
 
     return (
     return (
-      <Modal show={adminUsersContainer.state.isPasswordResetModalShown} onHide={adminUsersContainer.hidePasswordResetModal}>
+      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
         <Modal.Header className="modal-header" closeButton>
         <Modal.Header className="modal-header" closeButton>
           <Modal.Title>
           <Modal.Title>
             {t('admin:user_management.reset_password')}
             {t('admin:user_management.reset_password')}
@@ -107,13 +103,17 @@ class PasswordResetModal extends React.Component {
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
 const PasswordResetModalWrapper = (props) => {
 const PasswordResetModalWrapper = (props) => {
-  return createSubscribedElement(PasswordResetModal, props, [AppContainer, AdminUsersContainer]);
+  return createSubscribedElement(PasswordResetModal, props, [AppContainer]);
 };
 };
 
 
 PasswordResetModal.propTypes = {
 PasswordResetModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  userForPasswordResetModal: PropTypes.object,
+
 };
 };
 
 
 export default withTranslation()(PasswordResetModalWrapper);
 export default withTranslation()(PasswordResetModalWrapper);

+ 4 - 1
src/client/js/services/AdminUsersContainer.js

@@ -183,7 +183,10 @@ export default class AdminUsersContainer extends Container {
    * @memberOf AdminUsersContainer
    * @memberOf AdminUsersContainer
    */
    */
   async hidePasswordResetModal() {
   async hidePasswordResetModal() {
-    await this.setState({ isPasswordResetModalShown: false });
+    await this.setState({
+      isPasswordResetModalShown: false,
+      userForPasswordResetModal: null,
+    });
   }
   }
 
 
   /**
   /**

+ 0 - 43
src/migrations/20200414164011-add-image-attachment-path-to-user.js

@@ -1,43 +0,0 @@
-require('module-alias/register');
-const logger = require('@alias/logger')('growi:migrate:add-image-attachment-parh-to-user');
-
-const mongoose = require('mongoose');
-const config = require('@root/config/migrate');
-
-const { getModelSafely } = require('@commons/util/mongoose-utils');
-
-module.exports = {
-
-  async up(db) {
-    logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
-
-    const User = getModelSafely('User') || require('@server/models/user')();
-    require('@server/models/attachment')(); // for populating imageAttachment
-
-    const users = await User.find({ imageAttachment: { $exists: true } })
-      .populate({
-        path: 'imageAttachment',
-      })
-      .select('imageAttachment');
-
-    const requests = users.filter(user => user.imageAttachment).map((user) => {
-      return {
-        updateOne: {
-          filter: { _id: user._id },
-          update: { $set: { imageAttachmentPath: user.imageAttachment.filePathProxied } },
-        },
-      };
-    });
-
-    if (requests.length > 0) {
-      await db.collection('users').bulkWrite(requests);
-    }
-
-    logger.info('Migration has successfully applied');
-  },
-
-  down(db) {
-    db.collection('users').update({}, { $unset: 'imageAttachmentPath' });
-  },
-};

+ 22 - 4
src/server/models/user.js

@@ -6,6 +6,7 @@ const mongoose = require('mongoose');
 const mongoosePaginate = require('mongoose-paginate-v2');
 const mongoosePaginate = require('mongoose-paginate-v2');
 const path = require('path');
 const path = require('path');
 const uniqueValidator = require('mongoose-unique-validator');
 const uniqueValidator = require('mongoose-unique-validator');
+const md5 = require('md5');
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const crypto = require('crypto');
 const crypto = require('crypto');
@@ -17,7 +18,8 @@ module.exports = function(crowi) {
   const STATUS_DELETED = 4;
   const STATUS_DELETED = 4;
   const STATUS_INVITED = 5;
   const STATUS_INVITED = 5;
   const USER_PUBLIC_FIELDS = '_id image isEmailPublished isGravatarEnabled googleId name username email introduction'
   const USER_PUBLIC_FIELDS = '_id image isEmailPublished isGravatarEnabled googleId name username email introduction'
-  + 'status lang createdAt lastLoginAt admin imageAttachmentPath';
+  + 'status lang createdAt lastLoginAt admin imageUrlCached';
+  const IMAGE_POPULATION = { path: 'imageAttachment', select: 'filePathProxied' };
 
 
   const LANG_EN = 'en';
   const LANG_EN = 'en';
   const LANG_EN_US = 'en-US';
   const LANG_EN_US = 'en-US';
@@ -38,7 +40,7 @@ module.exports = function(crowi) {
     userId: String,
     userId: String,
     image: String,
     image: String,
     imageAttachment: { type: ObjectId, ref: 'Attachment' },
     imageAttachment: { type: ObjectId, ref: 'Attachment' },
-    imageAttachmentPath: String,
+    imageUrlCached: String,
     isGravatarEnabled: { type: Boolean, default: false },
     isGravatarEnabled: { type: Boolean, default: false },
     isEmailPublished: { type: Boolean, default: true },
     isEmailPublished: { type: Boolean, default: true },
     googleId: String,
     googleId: String,
@@ -186,6 +188,7 @@ module.exports = function(crowi) {
 
 
   userSchema.methods.updateIsGravatarEnabled = async function(isGravatarEnabled) {
   userSchema.methods.updateIsGravatarEnabled = async function(isGravatarEnabled) {
     this.isGravatarEnabled = isGravatarEnabled;
     this.isGravatarEnabled = isGravatarEnabled;
+    this.imageUrlCached = this.generateimageUrlCached();
     const userData = await this.save();
     const userData = await this.save();
     return userData;
     return userData;
   };
   };
@@ -221,7 +224,7 @@ module.exports = function(crowi) {
 
 
   userSchema.methods.updateImage = async function(attachment) {
   userSchema.methods.updateImage = async function(attachment) {
     this.imageAttachment = attachment;
     this.imageAttachment = attachment;
-    this.imageAttachmentPath = attachment.filePathProxied;
+    this.imageUrlCached = this.generateimageUrlCached();
     return this.save();
     return this.save();
   };
   };
 
 
@@ -237,10 +240,25 @@ module.exports = function(crowi) {
     }
     }
 
 
     this.imageAttachment = undefined;
     this.imageAttachment = undefined;
-    this.imageAttachmentPath = undefined;
+    this.imageUrlCached = this.generateimageUrlCached();
     return this.save();
     return this.save();
   };
   };
 
 
+  userSchema.methods.generateimageUrlCached = function() {
+    if (this.isGravatarEnabled) {
+      const email = this.email || '';
+      const hash = md5(email.trim().toLowerCase());
+      return `https://gravatar.com/avatar/${hash}`;
+    }
+    if (this.image) {
+      return this.image;
+    }
+    if (this.imageAttachment != null) {
+      return this.imageAttachment.filePathProxied;
+    }
+    return '/images/icons/user.svg';
+  };
+
   userSchema.methods.updateGoogleId = function(googleId, callback) {
   userSchema.methods.updateGoogleId = function(googleId, callback) {
     this.googleId = googleId;
     this.googleId = googleId;
     this.save((err, userData) => {
     this.save((err, userData) => {

+ 2 - 1
src/server/routes/index.js

@@ -124,8 +124,9 @@ module.exports = function(crowi, app) {
 
 
   app.get('/:id([0-9a-z]{24})'       , loginRequired , page.redirector);
   app.get('/:id([0-9a-z]{24})'       , loginRequired , page.redirector);
   app.get('/_r/:id([0-9a-z]{24})'    , loginRequired , page.redirector); // alias
   app.get('/_r/:id([0-9a-z]{24})'    , loginRequired , page.redirector); // alias
-  app.get('/attachment/:pageId/:fileName'  , loginRequired, attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below
   app.get('/attachment/:id([0-9a-z]{24})'  , loginRequired, attachment.api.get);
   app.get('/attachment/:id([0-9a-z]{24})'  , loginRequired, attachment.api.get);
+  app.get('/attachment/profile/:id([0-9a-z]{24})' , loginRequired, attachment.api.get);
+  app.get('/attachment/:pageId/:fileName', loginRequired, attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below
   app.get('/download/:id([0-9a-z]{24})'    , loginRequired, attachment.api.download);
   app.get('/download/:id([0-9a-z]{24})'    , loginRequired, attachment.api.download);
 
 
   app.get('/_search'                 , loginRequired , search.searchPage);
   app.get('/_search'                 , loginRequired , search.searchPage);

+ 14 - 14
yarn.lock

@@ -1277,10 +1277,10 @@
     "@types/yargs" "^15.0.0"
     "@types/yargs" "^15.0.0"
     chalk "^3.0.0"
     chalk "^3.0.0"
 
 
-"@kaishuu0123/markdown-it-fence@0.1.4", "@kaishuu0123/markdown-it-fence@^0.1.4":
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-0.1.4.tgz#759cf0dd80cca23a08e70b9cbb33c999cb23f3c3"
-  integrity sha512-u00GhVLpTeIbeflMKCozzaCAEmuwGngryomtbsYoyRwdYLfrH2nJHZa41+gwKLXsrq7Ii3N+Rei5GHaqHT3j5A==
+"@kaishuu0123/markdown-it-fence@^0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-0.2.0.tgz#f46722bfce4ab7eb3e051def5090dcae1bd6e36b"
+  integrity sha512-mdqKA+bXfJPl7gAg9tis8fGlea2oppBM068YbMDSXKWM6H18nVSZLrVKPHXpPWBgSv1ceeKkoWj8K1ntpIHlrw==
 
 
 "@lykmapipo/common@>=0.21.0":
 "@lykmapipo/common@>=0.21.0":
   version "0.21.0"
   version "0.21.0"
@@ -8338,23 +8338,23 @@ markdown-escapes@^1.0.0:
   resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.3.tgz#6155e10416efaafab665d466ce598216375195f5"
   resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.3.tgz#6155e10416efaafab665d466ce598216375195f5"
   integrity sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==
   integrity sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==
 
 
-markdown-it-blockdiag@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/markdown-it-blockdiag/-/markdown-it-blockdiag-1.0.3.tgz#7c2be967d21a17f559da5860b6179b34f29192a3"
-  integrity sha512-2y4C5L6V3twWhDdQLl3zzYvBiumKcS7D1CD8/OTN2Y57G1gE2ot86vTPrfljaf2Q8q6J1UgcG40zTjDGcHgHrg==
+markdown-it-blockdiag@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/markdown-it-blockdiag/-/markdown-it-blockdiag-1.1.1.tgz#f89d9a56c4ef693f4cec88fc62655fce23d15115"
+  integrity sha512-e8IvRZE7hS0eQqNMyJ8l3EI89swTUVvIkQ8MlmLA0DacumMauHCAfKgzxcZ2UMSj2wQPCmUUM9L9iAOAFcAByw==
   dependencies:
   dependencies:
-    "@kaishuu0123/markdown-it-fence" "0.1.4"
+    "@kaishuu0123/markdown-it-fence" "^0.2.0"
     pako "^1.0.6"
     pako "^1.0.6"
     paths "^0.1.1"
     paths "^0.1.1"
     url-join "^4.0.0"
     url-join "^4.0.0"
     utf8-bytes "0.0.1"
     utf8-bytes "0.0.1"
 
 
-markdown-it-drawio-viewer@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.1.3.tgz#e5f7fa9a1d200a8711e2aeb691551add413ffe33"
-  integrity sha512-FELyH+ko9sOAC6hHQqBYi4tI1Qv8HTzVPXsYuwTFy0NF5gY1A9oriN051rHn4UtyAAXEUIMquNNYYkxC9bhMVw==
+markdown-it-drawio-viewer@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.2.0.tgz#d47648c039f12e4c5ca706ed4d0f5dc19400c9a2"
+  integrity sha512-Hu9jxqKLVfFhk2T8J4ayaVbuoW2RSugRrXIsREMW7MMWFDciBgs9C8ADKaTav7JITY5fp7q6KJU7pqP/5dMRnA==
   dependencies:
   dependencies:
-    "@kaishuu0123/markdown-it-fence" "^0.1.4"
+    "@kaishuu0123/markdown-it-fence" "^0.2.0"
     xmldoc "^1.1.2"
     xmldoc "^1.1.2"
 
 
 markdown-it-emoji@^1.4.0:
 markdown-it-emoji@^1.4.0: