Przeglądaj źródła

Merge pull request #1142 from weseek/imprv/reactify-admin-user-management

Send Password Reset Request
Yuki Takei 6 lat temu
rodzic
commit
a117b6cadd

+ 2 - 0
resource/locales/en-US/translation.json

@@ -667,6 +667,7 @@
 
 
   "user_management": {
   "user_management": {
     "target_user": "Target User",
     "target_user": "Target User",
+    "new_password": "New Password",
     "invite_users": "Invite New Users",
     "invite_users": "Invite New Users",
     "emails": "Emails",
     "emails": "Emails",
     "invite_thru_email": "Send Invitation Email",
     "invite_thru_email": "Send Invitation Email",
@@ -686,6 +687,7 @@
     "unset": "No",
     "unset": "No",
     "temporary_password": "The created user has a temporary password",
     "temporary_password": "The created user has a temporary password",
     "send_temporary_password": "Be sure to copy the temporary password ON THIS SCREEN and send it to the user.",
     "send_temporary_password": "Be sure to copy the temporary password ON THIS SCREEN and send it to the user.",
+    "password_reset_message": "Let the user know the new password below and strongly recommend to change another one immediately.",
     "send_new_password": "Please send the new password to the user.",
     "send_new_password": "Please send the new password to the user.",
     "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
     "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
     "reset_password": "Reset Password",
     "reset_password": "Reset Password",

+ 2 - 0
resource/locales/ja/translation.json

@@ -650,6 +650,7 @@
 
 
   "user_management": {
   "user_management": {
     "target_user": "対象ユーザー",
     "target_user": "対象ユーザー",
+    "new_password": "新しいパスワード",
     "invite_users": "新規ユーザーの招待",
     "invite_users": "新規ユーザーの招待",
     "emails": "メールアドレス (複数行入力で複数人招待可能)",
     "emails": "メールアドレス (複数行入力で複数人招待可能)",
     "invite_thru_email": "招待をメールで送信",
     "invite_thru_email": "招待をメールで送信",
@@ -669,6 +670,7 @@
     "unset": "未設定",
     "unset": "未設定",
     "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
     "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
     "send_temporary_password": "招待メールを送っていない場合、この画面で必ず仮パスワードをコピーし、招待者へ連絡してください。",
     "send_temporary_password": "招待メールを送っていない場合、この画面で必ず仮パスワードをコピーし、招待者へ連絡してください。",
+    "password_reset_message": "対象ユーザーに下記のパスワードを伝え、すぐに新しく別のパスワードを設定するよう伝えてください。",
     "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
     "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
     "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
     "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
     "reset_password": "パスワードの再発行",
     "reset_password": "パスワードの再発行",

+ 56 - 0
src/client/js/components/Admin/Users/GiveAdminForm.jsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class AdminMenuForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+
+    };
+
+    this.handleSubmit = this.handleSubmit.bind(this);
+  }
+
+  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
+  handleSubmit(event) {
+    $(event.currentTarget).parent().submit();
+  }
+
+  render() {
+    const { t, appContainer, user } = this.props;
+
+    return (
+      <a className="px-4">
+        <form action={`/admin/user/${user._id}/makeAdmin`} method="post">
+          <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
+          <span onClick={this.handleSubmit}>
+            <i className="icon-fw icon-magic-wand"></i>{ t('user_management.give_admin_access') }
+          </span>
+        </form>
+      </a>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const AdminMenuFormWrapper = (props) => {
+  return createSubscribedElement(AdminMenuForm, props, [AppContainer]);
+};
+
+AdminMenuForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(AdminMenuFormWrapper);

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

@@ -0,0 +1,124 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import Modal from 'react-bootstrap/es/Modal';
+
+import toastError from '../../../util/apiNotification';
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class PasswordResetModal extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      temporaryPassword: [],
+      isPasswordResetDone: false,
+    };
+
+    this.returnModalBody = this.returnModalBody.bind(this);
+    this.returnModalFooter = this.returnModalFooter.bind(this);
+    this.resetPassword = this.resetPassword.bind(this);
+  }
+
+  async resetPassword() {
+    const { appContainer, user } = this.props;
+
+    const res = await appContainer.apiPost('/admin/users.resetPassword', { user_id: user._id });
+    if (res.ok) {
+      this.setState({ temporaryPassword: res.newPassword, isPasswordResetDone: true });
+    }
+    else {
+      toastError('Failed to reset password');
+    }
+  }
+
+  returnModalBody() {
+    const { t, user } = this.props;
+    return (
+      this.state.isPasswordResetDone
+        ? (
+          <div>
+            <p className="alert alert-danger">{ t('user_management.password_reset_message') }</p>
+            <p>
+              { t('user_management.target_user') }: <code>{ user.email }</code>
+            </p>
+            <p>
+              { t('user_management.new_password') }: <code>{ this.state.temporaryPassword }</code>
+            </p>
+          </div>
+        )
+        : (
+          <div>
+            <p>
+              { t('user_management.password_never_seen') }<br />
+              <span className="text-danger">{ t('user_management.send_new_password') }</span>
+            </p>
+            <p>
+              { t('user_management.target_user') }: <code>{ user.email }</code>
+            </p>
+            <button type="submit" className="btn btn-primary" onClick={this.resetPassword}>
+              { t('user_management.reset_password')}
+            </button>
+          </div>
+        )
+    );
+  }
+
+  returnModalFooter() {
+    return (
+      this.state.isPasswordResetDone
+        ? (
+          <div>
+            <button type="submit" className="btn btn-primary" onClick={this.props.onHideModal}>OK</button>
+          </div>
+        )
+        : (
+          ''
+        )
+    );
+  }
+
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Modal show={this.props.show} onHide={this.props.onHideModal}>
+        <Modal.Header className="modal-header" closeButton>
+          <Modal.Title>
+            { t('user_management.reset_password') }
+          </Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          {this.returnModalBody()}
+        </Modal.Body>
+        <Modal.Footer>
+          {this.returnModalFooter()}
+        </Modal.Footer>
+      </Modal>
+
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const PasswordResetModalWrapper = (props) => {
+  return createSubscribedElement(PasswordResetModal, props, [AppContainer]);
+};
+
+PasswordResetModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+  show: PropTypes.bool.isRequired,
+  onHideModal: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(PasswordResetModalWrapper);

+ 68 - 0
src/client/js/components/Admin/Users/RemoveAdminForm.jsx

@@ -0,0 +1,68 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class RemoveAdminForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+    };
+
+    this.handleSubmit = this.handleSubmit.bind(this);
+  }
+
+  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
+  handleSubmit(event) {
+    $(event.currentTarget).parent().submit();
+  }
+
+  render() {
+    const { t, user } = this.props;
+    const me = this.props.appContainer.me;
+
+    return (
+      <Fragment>
+        {user.username !== me
+          ? (
+            <a>
+              <form action={`/admin/user/${user._id}/removeFromAdmin`} method="post">
+                <input type="hidden" />
+                <span onClick={this.handleSubmit}>
+                  <i className="icon-fw icon-user-unfollow mb-2"></i>{ t('user_management.remove_admin_access') }
+                </span>
+              </form>
+            </a>
+          )
+          : (
+            <div className="px-4">
+              <i className="icon-fw icon-user-unfollow mb-2"></i>{ t('user_management.remove_admin_access') }
+              <p className="alert alert-danger">{ t('user_management.cannot_remove') }</p>
+            </div>
+          )
+        }
+      </Fragment>
+    );
+  }
+
+}
+
+/**
+* Wrapper component for using unstated
+*/
+const RemoveAdminFormWrapper = (props) => {
+  return createSubscribedElement(RemoveAdminForm, props, [AppContainer]);
+};
+
+RemoveAdminForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(RemoveAdminFormWrapper);

+ 72 - 0
src/client/js/components/Admin/Users/StatusActivateForm.jsx

@@ -0,0 +1,72 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class StatusActivateForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+
+    };
+
+    this.handleSubmit = this.handleSubmit.bind(this);
+  }
+
+  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
+  handleSubmit(event) {
+    $(event.currentTarget).parent().submit();
+  }
+
+  render() {
+    const { t, user, appContainer } = this.props;
+
+    return (
+      <Fragment>
+        {user.status === 1
+          ? (
+            <a>
+              <form action={`/admin/user/${user._id}/activate`} method="post">
+                <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
+                <span onClick={this.handleSubmit}>
+                  <i className="icon-fw icon-user-following"></i> { t('user_management.accept') }
+                </span>
+              </form>
+            </a>
+          )
+          : (
+            <a className="px-4">
+              <form action={`/admin/user/${user._id}/activate`} method="post">
+                <input type="hidden" />
+                <span onClick={this.handleSubmit}>
+                  <i className="icon-fw icon-user-following"></i> { t('user_management.accept') }
+                </span>
+              </form>
+            </a>
+          )
+        }
+      </Fragment>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const StatusActivateFormWrapper = (props) => {
+  return createSubscribedElement(StatusActivateForm, props, [AppContainer]);
+};
+
+StatusActivateForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(StatusActivateFormWrapper);

+ 69 - 0
src/client/js/components/Admin/Users/StatusSuspendedForm.jsx

@@ -0,0 +1,69 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class StatusSuspendedForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+
+    };
+
+    this.handleSubmit = this.handleSubmit.bind(this);
+  }
+
+  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
+  handleSubmit(event) {
+    $(event.currentTarget).parent().submit();
+  }
+
+  render() {
+    const { t, user } = this.props;
+    const me = this.props.appContainer.me;
+
+    return (
+      <Fragment>
+        {user.username !== me
+          ? (
+            <a>
+              <form action={`/admin/user/${user._id}/suspend`} method="post">
+                <input type="hidden" name="_csrf" value={this.props.appContainer.csrfToken} />
+                <span onClick={this.handleSubmit}>
+                  <i className="icon-fw icon-ban"></i>{ t('user_management.deactivate_account') }
+                </span>
+              </form>
+            </a>
+          )
+          : (
+            <div className="px-4">
+              <i className="icon-fw icon-ban mb-2"></i>{ t('user_management.deactivate_account') }
+              <p className="alert alert-danger">{ t('user_management.your_own') }</p>
+            </div>
+          )
+        }
+      </Fragment>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const StatusSuspendedFormWrapper = (props) => {
+  return createSubscribedElement(StatusSuspendedForm, props, [AppContainer]);
+};
+
+StatusSuspendedForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(StatusSuspendedFormWrapper);

+ 25 - 159
src/client/js/components/Admin/Users/UserMenu.jsx

@@ -2,119 +2,33 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import StatusActivateForm from './StatusActivateForm';
+import StatusSuspendedForm from './StatusSuspendedForm';
+import RemoveUserForm from './UserRemoveForm';
+import RemoveAdminForm from './RemoveAdminForm';
+import GiveAdminForm from './GiveAdminForm';
+
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 
 
 class UserMenu extends React.Component {
 class UserMenu extends React.Component {
 
 
+  constructor(props) {
+    super(props);
 
 
-  activateUser() {
-    const { appContainer } = this.props;
-
-    appContainer.apiPost('/admin/user/{userId}/activate');
-  }
-
-  susupendUser() {
-    const { appContainer } = this.props;
-
-    appContainer.apiPost('/admin/user/{userId}/suspend');
-  }
-
-  removeUser() {
-    const { appContainer } = this.props;
-
-    appContainer.apiPost('/admin/user/{user._id}/removeCompletely');
-  }
+    this.state = {
 
 
-  removeFromAdmin() {
-    const { appContainer } = this.props;
+    };
 
 
-    appContainer.apiPost('/admin/user/{user._id}/removeFromAdmin');
+    this.onPasswordResetClicked = this.onPasswordResetClicked.bind(this);
   }
   }
 
 
-  giveAdminAccess() {
-    const { appContainer } = this.props;
-
-    appContainer.apiPost('/admin/user/{user._id}/makeAdmin');
+  onPasswordResetClicked() {
+    this.props.onPasswordResetClicked(this.props.user);
   }
   }
 
 
-
   render() {
   render() {
     const { t, user } = this.props;
     const { t, user } = this.props;
-    const me = this.props.appContainer.me;
-
-    let contentOfStatus;
-    let adminMenu;
-
-    if (user.status === 1) {
-      contentOfStatus = (
-        <a className="mx-4" onClick={this.activateUser}>
-          <i className="icon-fw icon-user-following"></i> { t('user_management.accept') }
-        </a>
-      );
-    }
-    if (user.status === 2) {
-      contentOfStatus = (
-        user.username !== me
-          ? (
-            <a onClick={this.susupendUser}>
-              <i className="icon-fw icon-ban"></i>{ t('user_management.deactivate_account') }
-            </a>
-          )
-          : (
-            <div className="mx-4">
-              <i className="icon-fw icon-ban mb-2"></i>{ t('user_management.deactivate_account') }
-              <p className="alert alert-danger">{ t('user_management.your_own') }</p>
-            </div>
-          )
-      );
-    }
-    if (user.status === 3) {
-      contentOfStatus = (
-        <div>
-          <a className="mx-4" onClick={this.activateUser}>
-            <i className="icon-fw icon-action-redo"></i> { t('Undo') }
-          </a>
-          {/* label は同じだけど、こっちは論理削除 */}
-          <a className="mx-4" onClick={this.removeUser}>
-            <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
-          </a>
-        </div>
-      );
-    }
-    if (user.status === 1 || user.status === 5) {
-      contentOfStatus = (
-        <li className="dropdown-button">
-          <a className="mx-4" onClick={this.removeUser}>
-            <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
-          </a>
-        </li>
-      );
-    }
-
-    if (user.admin === true && user.status === 2) {
-      adminMenu = (
-        user.username !== me
-          ? (
-            <a onClick={this.removeFromAdmin}>
-              <i className="icon-fw icon-user-unfollow mb-2"></i> { t('user_management.remove_admin_access') }
-            </a>
-          )
-          : (
-            <div className="mx-4">
-              <i className="icon-fw icon-user-unfollow mb-2"></i>{ t('user_management.remove_admin_access') }
-              <p className="alert alert-danger">{ t('user_management.cannot_remove') }</p>
-            </div>
-          )
-      );
-    }
-    if (user.admin === false && user.status === 2) {
-      adminMenu = (
-        <a onClick={this.giveAdminAccess}>
-          <i className="icon-fw icon-magic-wand"></i>{ t('user_management.give_admin_access') }
-        </a>
-      );
-    }
 
 
     return (
     return (
       <Fragment>
       <Fragment>
@@ -124,75 +38,26 @@ class UserMenu extends React.Component {
           </button>
           </button>
           <ul className="dropdown-menu" role="menu">
           <ul className="dropdown-menu" role="menu">
             <li className="dropdown-header">{ t('user_management.edit_menu') }</li>
             <li className="dropdown-header">{ t('user_management.edit_menu') }</li>
-            <li>
-              <a
-                href="#"
-                data-user-id="{{ userId }}"
-                data-target="#admin-password-reset-modal"
-                data-toggle="modal"
-              >
-                <i className="icon-fw icon-key"></i>
-                { t('user_management.reset_password') }
+            <li onClick={this.onPasswordResetClicked}>
+              <a>
+                <i className="icon-fw icon-key"></i>{ t('user_management.reset_password') }
               </a>
               </a>
             </li>
             </li>
             <li className="divider"></li>
             <li className="divider"></li>
             <li className="dropdown-header">{ t('status') }</li>
             <li className="dropdown-header">{ t('status') }</li>
             <li>
             <li>
-              {contentOfStatus}
+              {(user.status === 1 || user.status === 3) && <StatusActivateForm user={user} />}
+              {user.status === 2 && <StatusSuspendedForm user={user} />}
+              {(user.status === 1 || user.status === 3 || user.status === 5) && <RemoveUserForm user={user} />}
             </li>
             </li>
             <li className="divider pl-0"></li>
             <li className="divider pl-0"></li>
             <li className="dropdown-header">{ t('user_management.administrator_menu') }</li>
             <li className="dropdown-header">{ t('user_management.administrator_menu') }</li>
-            <li>{adminMenu}</li>
+            <li>
+              {user.status === 2 && user.admin === true && <RemoveAdminForm user={user} />}
+              {user.status === 2 && user.admin === false && <GiveAdminForm user={user} />}
+            </li>
           </ul>
           </ul>
         </div>
         </div>
-        {/* password reset modal */}
-        <div className="modal fade" id="admin-password-reset-modal">
-          <div className="modal-dialog">
-            <div className="modal-content">
-              <div className="modal-header">
-                <button type="button" className="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-                <div className="modal-title">{ t('user_management.reset_password')}</div>
-              </div>
-
-              <div className="modal-body">
-                <p>
-                  { t('user_management.password_never_seen') }<br />
-                  <span className="text-danger">{ t('user_management.send_new_password') }</span>
-                </p>
-                <p>
-                  { t('user_management.target_user') }: <code>{ user.email }</code>
-                </p>
-                <button type="submit" value="" className="btn btn-primary">
-                  { t('user_management.reset_password')}
-                </button>
-              </div>
-            </div>
-          </div>
-        </div>
-        <div className="modal fade" id="admin-password-reset-modal-done">
-          <div className="modal-dialog">
-            <div className="modal-content">
-
-              <div className="modal-header">
-                <button type="button" className="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-                <div className="modal-title">{ t('user_management.reset_password') }</div>
-              </div>
-
-              <div className="modal-body">
-                <p className="alert alert-danger">Let the user know the new password below and strongly recommend to change another one immediately. </p>
-                <p>
-                Reset user: <code id="admin-password-reset-done-user"></code>
-                </p>
-                <p>
-                New password: <code id="admin-password-reset-done-password"></code>
-                </p>
-              </div>
-              <div className="modal-footer">
-                <button type="submit" className="btn btn-primary" data-dismiss="modal">OK</button>
-              </div>
-            </div>
-          </div>
-        </div>
       </Fragment>
       </Fragment>
     );
     );
   }
   }
@@ -207,7 +72,8 @@ UserMenu.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
-  user: PropTypes.array,
+  user: PropTypes.object.isRequired,
+  onPasswordResetClicked: PropTypes.func.isRequired,
 };
 };
 
 
 export default withTranslation()(UserMenuWrapper);
 export default withTranslation()(UserMenuWrapper);

+ 56 - 0
src/client/js/components/Admin/Users/UserRemoveForm.jsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+
+class UserRemoveForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+
+    };
+
+    this.handleSubmit = this.handleSubmit.bind(this);
+  }
+
+  // これは将来的にapiにするので。あとボタンにするとデザインがよくなかったので。
+  handleSubmit(event) {
+    $(event.currentTarget).parent().submit();
+  }
+
+  render() {
+    const { t, appContainer, user } = this.props;
+
+    return (
+      <a className="px-4">
+        <form action={`/admin/user/${user._id}/remove`} method="post">
+          <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
+          <span onClick={this.handleSubmit}>
+            <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
+          </span>
+        </form>
+      </a>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const UserRemoveFormWrapper = (props) => {
+  return createSubscribedElement(UserRemoveForm, props, [AppContainer]);
+};
+
+UserRemoveForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(UserRemoveFormWrapper);

+ 43 - 37
src/client/js/components/Admin/Users/UserTable.jsx

@@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
+
 import UserPicture from '../../User/UserPicture';
 import UserPicture from '../../User/UserPicture';
 import UserMenu from './UserMenu';
 import UserMenu from './UserMenu';
 
 
@@ -16,46 +17,51 @@ class UserTable extends React.Component {
     this.state = {
     this.state = {
 
 
     };
     };
-  }
 
 
+    this.getUserStatusLabel = this.getUserStatusLabel.bind(this);
+  }
 
 
-  render() {
-    const { t } = this.props;
-    let userStatusLabel;
+  /**
+   * user.statusをみてステータスのラベルを返す
+   * @param {string} userStatus
+   * @return ステータスラベル
+   */
+  getUserStatusLabel(userStatus) {
     let additionalClassName;
     let additionalClassName;
     let text;
     let text;
 
 
-    this.props.users.forEach((user) => {
-      userStatusLabel = (
-        <span className={`label ${additionalClassName}`}>
-          {text}
-        </span>
-      );
-
-      switch (user.status) {
-        case 1:
-          additionalClassName = 'label-info';
-          text = 'Approval Pending';
-          break;
-        case 2:
-          additionalClassName = 'label-success';
-          text = 'Active';
-          break;
-        case 3:
-          additionalClassName = 'label-warning';
-          text = 'Suspended';
-          break;
-        case 4:
-          additionalClassName = 'label-danger';
-          text = 'Deleted';
-          break;
-        case 5:
-          additionalClassName = 'label-info';
-          text = 'Invited';
-          break;
-      }
-    });
+    switch (userStatus) {
+      case 1:
+        additionalClassName = 'label-info';
+        text = 'Approval Pending';
+        break;
+      case 2:
+        additionalClassName = 'label-success';
+        text = 'Active';
+        break;
+      case 3:
+        additionalClassName = 'label-warning';
+        text = 'Suspended';
+        break;
+      case 4:
+        additionalClassName = 'label-danger';
+        text = 'Deleted';
+        break;
+      case 5:
+        additionalClassName = 'label-info';
+        text = 'Invited';
+        break;
+    }
+
+    return (
+      <span className={`label ${additionalClassName}`}>
+        {text}
+      </span>
+    );
+  }
 
 
+  render() {
+    const { t } = this.props;
 
 
     return (
     return (
       <Fragment>
       <Fragment>
@@ -82,7 +88,7 @@ class UserTable extends React.Component {
                     <UserPicture user={user} className="picture img-circle" />
                     <UserPicture user={user} className="picture img-circle" />
                     {user.admin && <span className="label label-inverse label-admin ml-2">{ t('administrator') }</span>}
                     {user.admin && <span className="label label-inverse label-admin ml-2">{ t('administrator') }</span>}
                   </td>
                   </td>
-                  <td>{userStatusLabel}</td>
+                  <td>{this.getUserStatusLabel(user.status)}</td>
                   <td>
                   <td>
                     <strong>{user.username}</strong>
                     <strong>{user.username}</strong>
                   </td>
                   </td>
@@ -93,7 +99,7 @@ class UserTable extends React.Component {
                     { user.lastLoginAt && <span>{dateFnsFormat(new Date(user.lastLoginAt), 'YYYY-MM-DD HH:mm')}</span> }
                     { user.lastLoginAt && <span>{dateFnsFormat(new Date(user.lastLoginAt), 'YYYY-MM-DD HH:mm')}</span> }
                   </td>
                   </td>
                   <td>
                   <td>
-                    <UserMenu user={user} />
+                    <UserMenu user={user} onPasswordResetClicked={this.props.onPasswordResetClicked} />
                   </td>
                   </td>
                 </tr>
                 </tr>
               );
               );
@@ -115,7 +121,7 @@ UserTable.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   users: PropTypes.array.isRequired,
   users: PropTypes.array.isRequired,
-
+  onPasswordResetClicked: PropTypes.func.isRequired,
 };
 };
 
 
 export default withTranslation()(UserTableWrapper);
 export default withTranslation()(UserTableWrapper);

+ 29 - 0
src/client/js/components/Admin/Users/Users.jsx

@@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import PasswordResetModal from './PasswordResetModal';
 import PaginationWrapper from '../../PaginationWrapper';
 import PaginationWrapper from '../../PaginationWrapper';
 import InviteUserControl from './InviteUserControl';
 import InviteUserControl from './InviteUserControl';
 import UserTable from './UserTable';
 import UserTable from './UserTable';
@@ -15,11 +16,15 @@ class UserPage extends React.Component {
     super();
     super();
 
 
     this.state = {
     this.state = {
+      userForPasswordResetModal: null,
       users: [],
       users: [],
       activePage: 1,
       activePage: 1,
       pagingLimit: Infinity,
       pagingLimit: Infinity,
+      isPasswordResetModalShown: false,
     };
     };
 
 
+    this.showPasswordResetModal = this.showPasswordResetModal.bind(this);
+    this.hidePasswordResetModal = this.hidePasswordResetModal.bind(this);
   }
   }
 
 
   // TODO unstatedContainerを作ってそこにリファクタすべき
   // TODO unstatedContainerを作ってそこにリファクタすべき
@@ -32,12 +37,35 @@ class UserPage extends React.Component {
     });
     });
   }
   }
 
 
+  /**
+   * passwordリセットモーダルが開き、userが渡される
+   * @param {object} user
+   *
+   */
+  showPasswordResetModal(user) {
+    this.setState({
+      isPasswordResetModalShown: true,
+      userForPasswordResetModal: user,
+    });
+  }
+
+  hidePasswordResetModal() {
+    this.setState({ isPasswordResetModalShown: false });
+  }
+
 
 
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
 
 
     return (
     return (
       <Fragment>
       <Fragment>
+        { this.state.userForPasswordResetModal && (
+          <PasswordResetModal
+            user={this.state.userForPasswordResetModal}
+            show={this.state.isPasswordResetModalShown}
+            onHideModal={this.hidePasswordResetModal}
+          />
+        ) }
         <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">
@@ -47,6 +75,7 @@ class UserPage extends React.Component {
         </p>
         </p>
         <UserTable
         <UserTable
           users={this.state.users}
           users={this.state.users}
+          onPasswordResetClicked={this.showPasswordResetModal}
         />
         />
         <PaginationWrapper
         <PaginationWrapper
           activePage={this.state.activePage}
           activePage={this.state.activePage}

+ 14 - 0
src/server/views/admin/users.html

@@ -12,6 +12,20 @@
 
 
 {% block content_main %}
 {% block content_main %}
 <div class="content-main">
 <div class="content-main">
+    {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
   <div class="col-md-3">
   <div class="col-md-3">
     {% include './widget/menu.html' with {current: 'user'} %}
     {% include './widget/menu.html' with {current: 'user'} %}
   </div>
   </div>