From 1cb5ae54c5b568762615b5ac078f952e70836ad3 Mon Sep 17 00:00:00 2001 From: Weihao Chen <53147830+1340908470@users.noreply.github.com> Date: Wed, 2 Jun 2021 13:39:01 +0800 Subject: [PATCH] feat: add "forget password" [front & backend] (#75) * feat: add "forget password" [front & backend] Signed-off-by: Weihao <1340908470@qq.com> * fix: verification code can be sent even if no mobile phone or email is selected refactor: forgetPassword -> forget; GetEmailAndPhoneByUsername -> GetEmailAndPhone; remove useless note Signed-off-by: Weihao <1340908470@qq.com> --- authz/authz.go | 1 + controllers/auth.go | 62 +++- controllers/user.go | 51 ++- routers/router.go | 1 + web/src/App.js | 4 + web/src/auth/AuthBackend.js | 8 + web/src/auth/ForgetPage.js | 499 ++++++++++++++++++++++++++++ web/src/auth/SelfForgetPage.js | 32 ++ web/src/component/CountDownInput.js | 15 +- web/src/locales/en.json | 25 ++ web/src/locales/zh.json | 25 ++ 11 files changed, 716 insertions(+), 7 deletions(-) create mode 100644 web/src/auth/ForgetPage.js create mode 100644 web/src/auth/SelfForgetPage.js diff --git a/authz/authz.go b/authz/authz.go index 07d8c040..c9444aac 100644 --- a/authz/authz.go +++ b/authz/authz.go @@ -72,6 +72,7 @@ m = (r.subOwner == p.subOwner || p.subOwner == "*") && \ ruleText := ` p, built-in, *, *, *, *, * p, *, *, POST, /api/signup, *, * +p, *, *, POST, /api/get-email-and-phone, *, * p, *, *, POST, /api/login, *, * p, *, *, GET, /api/get-app-login, *, * p, *, *, POST, /api/logout, *, * diff --git a/controllers/auth.go b/controllers/auth.go index fd7461fc..e0487bd9 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -17,6 +17,7 @@ package controllers import ( "encoding/json" "fmt" + "strings" "github.com/astaxie/beego" "github.com/casdoor/casdoor/idp" @@ -106,8 +107,65 @@ func (c *ApiController) Login() { } } - password := form.Password - user, msg := object.CheckUserLogin(form.Organization, form.Username, password) + var user *object.User + var msg string + + if form.Password == "" { + var verificationCodeType string + + // check result through Email or Phone + if strings.Contains(form.Email, "@") { + verificationCodeType = "email" + checkResult := object.CheckVerificationCode(form.Email, form.EmailCode) + if len(checkResult) != 0 { + responseText := fmt.Sprintf("Email%s", checkResult) + c.ResponseError(responseText) + return + } + } else { + verificationCodeType = "phone" + checkPhone := fmt.Sprintf("+%s%s", form.PhonePrefix, form.Email) + checkResult := object.CheckVerificationCode(checkPhone, form.EmailCode) + if len(checkResult) != 0 { + responseText := fmt.Sprintf("Phone%s", checkResult) + c.ResponseError(responseText) + return + } + } + + // get user + var userId string + if form.Username == "" { + userId, _ = c.RequireSignedIn() + } else { + userId = fmt.Sprintf("%s/%s", form.Organization, form.Username) + } + + user = object.GetUser(userId) + if user == nil { + c.ResponseError("No such user.") + return + } + + // disable the verification code + switch verificationCodeType { + case "email": + if user.Email != form.Email { + c.ResponseError("wrong email!") + } + object.DisableVerificationCode(form.Email) + break + case "phone": + if user.Phone != form.Email { + c.ResponseError("wrong phone!") + } + object.DisableVerificationCode(form.Email) + break + } + } else { + password := form.Password + user, msg = object.CheckUserLogin(form.Organization, form.Username, password) + } if msg != "" { resp = &Response{Status: "error", Msg: msg, Data: ""} diff --git a/controllers/user.go b/controllers/user.go index c584d0c3..91475c8c 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -111,6 +111,43 @@ func (c *ApiController) DeleteUser() { c.ServeJSON() } +// @Title GetEmailAndPhone +// @Description get email and phone by username +// @Param username formData string true "The username of the user" +// @Param organization formData string true "The organization of the user" +// @Success 200 {object} controllers.Response The Response object +// @router /get-email-and-phone [post] +func (c *ApiController) GetEmailAndPhone() { + var resp Response + + var form RequestForm + err := json.Unmarshal(c.Ctx.Input.RequestBody, &form) + if err != nil { + panic(err) + } + + // get user + var userId string + if form.Username == "" { + userId, _ = c.RequireSignedIn() + } else { + userId = fmt.Sprintf("%s/%s", form.Organization, form.Username) + } + user := object.GetUser(userId) + if user == nil { + c.ResponseError("No such user.") + return + } + + phone := user.Phone + email := user.Email + + resp = Response{Status: "ok", Msg: "", Data: phone, Data2: email} + + c.Data["json"] = resp + c.ServeJSON() +} + // @Title SetPassword // @Description set password // @Param userOwner formData string true "The owner of the user" @@ -158,10 +195,14 @@ func (c *ApiController) SetPassword() { return } - msg := object.CheckPassword(targetUser, oldPassword) - if msg != "" { - c.ResponseError(msg) - return + if oldPassword != "" { + msg := object.CheckPassword(targetUser, oldPassword) + if msg != "" { + c.ResponseError(msg) + return + } + } else { + } if strings.Index(newPassword, " ") >= 0 { @@ -174,6 +215,8 @@ func (c *ApiController) SetPassword() { return } + c.SetSessionUser("") + targetUser.Password = newPassword object.SetUserField(targetUser, "password", targetUser.Password) c.Data["json"] = Response{Status: "ok"} diff --git a/routers/router.go b/routers/router.go index 5e906c4f..f2af5723 100644 --- a/routers/router.go +++ b/routers/router.go @@ -60,6 +60,7 @@ func initAPI() { beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser") beego.Router("/api/upload-avatar", &controllers.ApiController{}, "POST:UploadAvatar") beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") + beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "POST:GetEmailAndPhone") beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode") beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone") beego.Router("/api/get-human-check", &controllers.ApiController{}, "GET:GetHumanCheck") diff --git a/web/src/App.js b/web/src/App.js index f5304c87..cf6f0e7d 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -38,6 +38,8 @@ import SignupPage from "./auth/SignupPage"; import ResultPage from "./auth/ResultPage"; import LoginPage from "./auth/LoginPage"; import SelfLoginPage from "./auth/SelfLoginPage"; +import SelfForgetPage from "./auth/SelfForgetPage"; +import ForgetPage from "./auth/ForgetPage"; import * as AuthBackend from "./auth/AuthBackend"; import AuthCallback from "./auth/AuthCallback"; import SelectLanguageBox from './SelectLanguageBox'; @@ -374,6 +376,8 @@ class App extends Component { this.renderHomeIfLoggedIn()}/> this.renderHomeIfLoggedIn()}/> + this.renderHomeIfLoggedIn()}/> + this.renderHomeIfLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> this.renderLoginIfNotLoggedIn()}/> diff --git a/web/src/auth/AuthBackend.js b/web/src/auth/AuthBackend.js index 9497df3b..644f33fa 100644 --- a/web/src/auth/AuthBackend.js +++ b/web/src/auth/AuthBackend.js @@ -29,6 +29,14 @@ export function signup(values) { }).then(res => res.json()); } +export function getEmailAndPhone(values) { + return fetch(`${authConfig.serverUrl}/api/get-email-and-phone`, { + method: "POST", + credentials: "include", + body: JSON.stringify(values), + }).then((res) => res.json()); +} + function oAuthParamsToQuery(oAuthParams) { return `?clientId=${oAuthParams.clientId}&responseType=${oAuthParams.responseType}&redirectUri=${oAuthParams.redirectUri}&scope=${oAuthParams.scope}&state=${oAuthParams.state}`; } diff --git a/web/src/auth/ForgetPage.js b/web/src/auth/ForgetPage.js new file mode 100644 index 00000000..a1d22555 --- /dev/null +++ b/web/src/auth/ForgetPage.js @@ -0,0 +1,499 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from "react"; +import { Button, Col, Divider, Form, Select, Input, Row, Steps } from "antd"; +import * as AuthBackend from "./AuthBackend"; +import * as ApplicationBackend from "../backend/ApplicationBackend"; +import * as Util from "./Util"; +import * as Setting from "../Setting"; +import i18next from "i18next"; +import { CountDownInput } from "../component/CountDownInput"; +import * as UserBackend from "../backend/UserBackend"; +import { + CheckCircleOutlined, + KeyOutlined, + LockOutlined, + SolutionOutlined, + UserOutlined, +} from "@ant-design/icons"; + +const { Step } = Steps; +const { Option } = Select; + +class ForgetPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + account: props.account, + applicationName: + props.applicationName !== undefined + ? props.applicationName + : props.match === undefined + ? null + : props.match.params.applicationName, + application: null, + msg: null, + userId: "", + username: "", + email: "", + token: "", + phone: "", + emailCode: "", + phoneCode: "", + verifyType: "", // "email" or "phone" + current: 0, + }; + } + + UNSAFE_componentWillMount() { + if (this.state.applicationName !== undefined) { + this.getApplication(); + } else { + Util.showMessage( + "error", + i18next.t(`forget:Unknown forgot type: `) + this.state.type + ); + } + } + + getApplication() { + if (this.state.applicationName === null) { + return; + } + + ApplicationBackend.getApplication("admin", this.state.applicationName).then( + (application) => { + this.setState({ + application: application, + }); + } + ); + } + + getApplicationObj() { + if (this.props.application !== undefined) { + return this.props.application; + } else { + return this.state.application; + } + } + + onFinishStep1(values) { + AuthBackend.getEmailAndPhone(values).then((res) => { + if (res.status === "ok") { + this.setState({ + username: values.username, + phone: res.data.toString(), + email: res.data2.toString(), + current: 1, + }); + } else { + Setting.showMessage("error", i18next.t(`signup:${res.msg}`)); + } + }); + } + + onFinishStep2(values) { + values.phonePrefix = this.state.application?.organizationObj.phonePrefix; + values.username = this.state.username; + values.type = "login" + const oAuthParams = Util.getOAuthGetParameters(); + AuthBackend.login(values, oAuthParams).then(res => { + if (res.status === "ok") { + this.setState({current: 2, userId: res.data}) + } else { + Setting.showMessage("error", i18next.t(`signup:${res.msg}`)); + } + }) + } + + onFinish(values) { + values.username = this.state.username; + values.userOwner = this.state.application?.organizationObj.name + UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword).then(res => { + if (res.status === "ok") { + Setting.goToLogin(this, this.state.application); + } else { + Setting.showMessage("error", i18next.t(`signup:${res.msg}`)); + } + }) + } + + onFinishFailed(values, errorFields) {} + + onChange = (current) => { + this.setState({ current: current }); + }; + + renderForm(application) { + return ( + <> + {/* STEP 1: input username -> get email & phone */} +