modify password (#176)

Co-authored-by: wenyu.dai <“wenyu.dai@qq.com”>
diff --git a/package.json b/package.json
index e92c0fe..2c08861 100755
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
   },
   "dependencies": {
     "@babel/polyfill": "^7.0.0-beta.36",
-    "antd": "^3.8.2",
+    "antd": "^3.26.19",
     "classnames": "^2.2.5",
     "dayjs": "^1.8.17",
     "dva": "^2.2.3",
diff --git a/src/components/GlobalHeader/index.js b/src/components/GlobalHeader/index.js
index cbf7f87..5288c2b 100644
--- a/src/components/GlobalHeader/index.js
+++ b/src/components/GlobalHeader/index.js
@@ -15,59 +15,100 @@
  * limitations under the License.
  */
 
-import React, { PureComponent } from 'react';
-import { Dropdown, Icon, Menu } from 'antd';
-import styles from './index.less';
-import { getIntlContent, getCurrentLocale } from '../../utils/IntlUtils'
-import { emit } from '../../utils/emit';
+import React, { PureComponent } from "react";
+import { Dropdown, Form, Icon, Input, Menu, Modal } from "antd";
+import { connect } from "dva";
+import styles from "./index.less";
+import { getIntlContent, getCurrentLocale } from "../../utils/IntlUtils";
+import { emit } from "../../utils/emit";
 
-const TranslationOutlinedSvg = () => <svg viewBox="64 64 896 896" focusable="false" data-icon="translation" width="1em" height="1em" fill="currentColor" aria-hidden="true"><defs><style /></defs><path d="M140 188h584v164h76V144c0-17.7-14.3-32-32-32H96c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h544v-76H140V188z" /><path d="M414.3 256h-60.6c-3.4 0-6.4 2.2-7.6 5.4L219 629.4c-.3.8-.4 1.7-.4 2.6 0 4.4 3.6 8 8 8h55.1c3.4 0 6.4-2.2 7.6-5.4L322 540h196.2L422 261.4a8.42 8.42 0 00-7.7-5.4zm12.4 228h-85.5L384 360.2 426.7 484zM936 528H800v-93c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v93H592c-13.3 0-24 10.7-24 24v176c0 13.3 10.7 24 24 24h136v152c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V752h136c13.3 0 24-10.7 24-24V552c0-13.3-10.7-24-24-24zM728 680h-88v-80h88v80zm160 0h-88v-80h88v80z" /></svg>
-const TranslationOutlined = props => <Icon component={TranslationOutlinedSvg} {...props} />;
+const TranslationOutlinedSvg = () => (
+  <svg
+    viewBox="64 64 896 896"
+    focusable="false"
+    data-icon="translation"
+    width="1em"
+    height="1em"
+    fill="currentColor"
+    aria-hidden="true"
+  >
+    <defs>
+      <style />
+    </defs>
+    <path d="M140 188h584v164h76V144c0-17.7-14.3-32-32-32H96c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h544v-76H140V188z" />
+    <path d="M414.3 256h-60.6c-3.4 0-6.4 2.2-7.6 5.4L219 629.4c-.3.8-.4 1.7-.4 2.6 0 4.4 3.6 8 8 8h55.1c3.4 0 6.4-2.2 7.6-5.4L322 540h196.2L422 261.4a8.42 8.42 0 00-7.7-5.4zm12.4 228h-85.5L384 360.2 426.7 484zM936 528H800v-93c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v93H592c-13.3 0-24 10.7-24 24v176c0 13.3 10.7 24 24 24h136v152c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V752h136c13.3 0 24-10.7 24-24V552c0-13.3-10.7-24-24-24zM728 680h-88v-80h88v80zm160 0h-88v-80h88v80z" />
+  </svg>
+);
+const TranslationOutlined = props => (
+  <Icon component={TranslationOutlinedSvg} {...props} />
+);
 
-export default class GlobalHeader extends PureComponent {
+@connect(({ manage, loading }) => ({
+  manage,
+  loading: loading.effects["manage/update"]
+}))
+@Form.create({})
+class GlobalHeader extends PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       menu: (
         <Menu onClick={this.handleLocalesValueChange}>
-          <Menu.Item key='0'>
+          <Menu.Item key="0">
             <span>English</span>
           </Menu.Item>
-          <Menu.Item key='1'>
+          <Menu.Item key="1">
             <span>中文</span>
           </Menu.Item>
         </Menu>
       ),
-      localeName: window.sessionStorage.getItem('locale') ? window.sessionStorage.getItem('locale') : 'en-US',
-      userName: window.sessionStorage.getItem('userName')
-    }
+      localeName: window.sessionStorage.getItem("locale")
+        ? window.sessionStorage.getItem("locale")
+        : "en-US",
+      userName: window.sessionStorage.getItem("userName"),
+      visible: false
+    };
   }
 
   handleLocalesValueChange = value => {
     const { changeLocalName } = this.props;
-    if (value.key === '0') {
-      emit.emit('change_language', 'en-US');
-      window.sessionStorage.setItem('locale', 'en-US');
+    if (value.key === "0") {
+      emit.emit("change_language", "en-US");
+      window.sessionStorage.setItem("locale", "en-US");
       this.setState({
-        localeName: 'en-Us'
+        localeName: "en-Us"
       });
-      changeLocalName('en-Us');
+      changeLocalName("en-Us");
     } else {
-      emit.emit('change_language', 'zh-CN');
-      window.sessionStorage.setItem('locale', 'zh-CN');
+      emit.emit("change_language", "zh-CN");
+      window.sessionStorage.setItem("locale", "zh-CN");
       this.setState({
-        localeName: 'zh-CN'
+        localeName: "zh-CN"
       });
-      changeLocalName('zh-CN');
+      changeLocalName("zh-CN");
     }
     getCurrentLocale(this.state.localeName);
-  }
+  };
 
   render() {
-    const { onLogout } = this.props;
-    const { userName } = this.state;
+    const {
+      onLogout,
+      form: { getFieldDecorator, resetFields, validateFields, getFieldValue },
+      dispatch,
+      loading
+    } = this.props;
+    const { userName, visible } = this.state;
     const menu = (
       <Menu>
+        <Menu.Item
+          key="1"
+          onClick={() => {
+            this.setState({ visible: true });
+          }}
+        >
+          <Icon type="form" />{" "}
+          {getIntlContent("SHENYU.GLOBALHEADER.CHANGE.PASSWORD")}
+        </Menu.Item>
         <Menu.Item key="0" onClick={onLogout}>
           <Icon type="logout" /> {getIntlContent("SHENYU.GLOBALHEADER.LOGOUT")}
         </Menu.Item>
@@ -75,17 +116,132 @@
     );
     return (
       <div className={styles.header}>
-        <Dropdown placement="bottomCenter" overlay={this.state.menu} trigger={['click']}>
+        <Dropdown
+          placement="bottomCenter"
+          overlay={this.state.menu}
+          trigger={["click"]}
+        >
           <TranslationOutlined />
         </Dropdown>
         <div className={styles.right}>
           <Dropdown overlay={menu}>
             <span>
-              <Icon type="user" />{userName}<Icon type="down" />
+              <Icon type="user" />
+              {userName}
+              <Icon type="down" />
             </span>
           </Dropdown>
         </div>
+        <Modal
+          title={getIntlContent("SHENYU.GLOBALHEADER.CHANGE.PASSWORD")}
+          visible={visible}
+          forceRender
+          okButtonProps={{
+            loading
+          }}
+          onCancel={() => {
+            this.setState({ visible: false });
+            resetFields();
+          }}
+          onOk={() => {
+            validateFields((errors, values) => {
+              if (!errors) {
+                dispatch({
+                  type: "manage/updatePassword",
+                  payload: {
+                    id: window.sessionStorage.getItem("userId"),
+                    userName: window.sessionStorage.getItem("userName"),
+                    password: values.password
+                  },
+                  callback: () => {
+                    this.setState({ visible: false });
+                    resetFields();
+                  }
+                });
+              }
+            });
+          }}
+        >
+          <Form labelCol={{ span: 8 }} wrapperCol={{ span: 14 }}>
+            <Form.Item
+              required
+              label={getIntlContent("SHENYU.GLOBALHEADER.NEW.PASSWORD")}
+              extra={getIntlContent("SHENYU.GLOBALHEADER.PASSWORD.EXTRA")}
+            >
+              {getFieldDecorator("password", {
+                rules: [
+                  {
+                    validator(rule, value, callback) {
+                      const confirmPassword = getFieldValue("confirmPassword");
+                      if (!value) {
+                        callback(
+                          getIntlContent(
+                            "SHENYU.GLOBALHEADER.PASSWORD.REQUIRED"
+                          )
+                        );
+                        return;
+                      }
+                      if (value.length < 8 || value.length > 16) {
+                        callback(
+                          getIntlContent("SHENYU.GLOBALHEADER.PASSWORD.LENGTH")
+                        );
+                        return;
+                      }
+                      if (
+                        !/(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^a-zA-Z0-9])/.test(
+                          value
+                        )
+                      ) {
+                        callback(
+                          getIntlContent("SHENYU.GLOBALHEADER.PASSWORD.RULE")
+                        );
+                        return;
+                      }
+                      if (confirmPassword) {
+                        validateFields(["confirmPassword"], { force: true });
+                      }
+                      callback();
+                    }
+                  }
+                ]
+              })(<Input.Password />)}
+            </Form.Item>
+            <Form.Item
+              label={getIntlContent("SHENYU.GLOBALHEADER.CONFIRM.PASSWORD")}
+              required
+            >
+              {getFieldDecorator("confirmPassword", {
+                rules: [
+                  {
+                    validator(rule, value, callback) {
+                      const password = getFieldValue("password");
+                      if (!value) {
+                        callback(
+                          getIntlContent(
+                            "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD.REQUIRED"
+                          )
+                        );
+                        return;
+                      }
+                      if (password !== value) {
+                        callback(
+                          getIntlContent(
+                            "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD.RULE"
+                          )
+                        );
+                        return;
+                      }
+                      callback();
+                    }
+                  }
+                ]
+              })(<Input.Password />)}
+            </Form.Item>
+          </Form>
+        </Modal>
       </div>
     );
   }
 }
+
+export default GlobalHeader;
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index e76a722..297e5f9 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -1,5 +1,19 @@
 {
   "SHENYU.GLOBALHEADER.LOGOUT": "Logout",
+  "SHENYU.GLOBALHEADER.CHANGE.PASSWORD": "Change Password",
+  "SHENYU.GLOBALHEADER.NEW.PASSWORD": "New Password",
+  "SHENYU.GLOBALHEADER.PASSWORD.EXTRA":
+    "The password is 8 ~ 16 characters long and must contain uppercase letters, lowercase letters, numbers and special characters",
+  "SHENYU.GLOBALHEADER.PASSWORD.REQUIRED": "Please enter a new password",
+  "SHENYU.GLOBALHEADER.PASSWORD.LENGTH":
+    "The password length is 8 ~ 16 characters",
+  "SHENYU.GLOBALHEADER.PASSWORD.RULE":
+    "The password must contain uppercase letters, lowercase letters, numbers and special characters",
+  "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD": "Confirm Password",
+  "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD.REQUIRED":
+    "Please enter the confirm password",
+  "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD.RULE":
+    "The two passwords are inconsistent",
   "SHENYU.HOME.WELCOME":
     "Welcome to the Apache ShenYu Gateway Management System",
   "SHENYU.SIDERMENU.LOGO": "Apache ShenYu Gateway Managment",
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index 5d27336..b60ff7c 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -1,5 +1,16 @@
 {
   "SHENYU.GLOBALHEADER.LOGOUT": "退出登录",
+  "SHENYU.GLOBALHEADER.CHANGE.PASSWORD": "修改密码",
+  "SHENYU.GLOBALHEADER.NEW.PASSWORD": "新密码",
+  "SHENYU.GLOBALHEADER.PASSWORD.EXTRA":
+    "密码长度为8~16个字符,必须包含大写字母、小写字母、数字、特殊字符",
+  "SHENYU.GLOBALHEADER.PASSWORD.REQUIRED": "请输入新密码",
+  "SHENYU.GLOBALHEADER.PASSWORD.LENGTH": "密码长度为8~16个字符",
+  "SHENYU.GLOBALHEADER.PASSWORD.RULE":
+    "密码必须包含大写字母、小写字母、数字、特殊字符",
+  "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD": "确认密码",
+  "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD.REQUIRED": "请输入确认密码",
+  "SHENYU.GLOBALHEADER.CONFIRM.PASSWORD.RULE": "两次密码不一致",
   "SHENYU.HOME.WELCOME": "欢迎登录Apache ShenYu网关管理系统",
   "SHENYU.COMMON.OPEN": "开启",
   "SHENYU.COMMON.CLOSE": "关闭",
diff --git a/src/models/login.js b/src/models/login.js
index a98bc3d..e6a0f16 100644
--- a/src/models/login.js
+++ b/src/models/login.js
@@ -33,7 +33,6 @@
       const response = yield call(queryLogin, payload);
 
       // Login successfully
-
       if (response.data) {
         yield put({
           type: "changeLoginStatus",
@@ -44,6 +43,7 @@
         });
         window.sessionStorage.setItem("token", response.data.token);
         window.sessionStorage.setItem("userName", response.data.userName);
+        window.sessionStorage.setItem("userId", response.data.id);
         /* const urlParams = new URL(window.location.href);
          const params = getPageQuery();
          let { redirect } = params;
@@ -80,6 +80,7 @@
       });
       window.sessionStorage.removeItem("token");
       window.sessionStorage.removeItem("userName");
+      window.sessionStorage.removeItem("userId");
       yield put(
         routerRedux.push({
           pathname: "/user/login"
diff --git a/src/models/manage.js b/src/models/manage.js
index 9e2f0d2..41e514b 100644
--- a/src/models/manage.js
+++ b/src/models/manage.js
@@ -21,7 +21,8 @@
   findUser,
   updateUser,
   deleteUser,
-  addUser
+  addUser,
+  updatePassword
 } from "../services/api";
 import { getIntlContent } from "../utils/IntlUtils";
 
@@ -101,12 +102,25 @@
           getIntlContent("SHENYU.COMMON.RESPONSE.UPDATE.SUCCESS")
         );
         callback();
-        yield put({ type: "reload", fetchValue });
+        if (fetchValue) {
+          yield put({ type: "reload", fetchValue });
+        }
       } else {
         message.warn(json.message);
       }
     },
-
+    *updatePassword(params, { call }) {
+      const { payload, callback } = params;
+      const json = yield call(updatePassword, payload);
+      if (json.code === 200) {
+        message.success(
+          getIntlContent("SHENYU.COMMON.RESPONSE.UPDATE.SUCCESS")
+        );
+        callback();
+      } else {
+        message.warn(json.message);
+      }
+    },
     *reload(params, { put }) {
       const { fetchValue } = params;
       const { userName, currentPage, pageSize } = fetchValue;
diff --git a/src/services/api.js b/src/services/api.js
index 2e10a66..6779a6d 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -49,6 +49,18 @@
     }
   });
 }
+
+/* update password */
+export async function updatePassword(params) {
+  return request(`${baseUrl}/dashboardUser/modify-password/${params.id}`, {
+    method: `PUT`,
+    body: {
+      userName: params.userName,
+      password: params.password
+    }
+  });
+}
+
 /* get all metadata */
 export async function getAllMetadata(params) {
   const { appName, currentPage, pageSize } = params;