feat: optimize the "forget password" page (#1709)

This commit is contained in:
Yaodong Yu
2023-04-06 23:06:18 +08:00
committed by GitHub
parent e1842f6b80
commit b99a0c3ca2
11 changed files with 145 additions and 61 deletions

View File

@ -108,6 +108,7 @@ p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, * p, *, *, POST, /api/send-verification-code, *, *
p, *, *, GET, /api/get-captcha, *, * p, *, *, GET, /api/get-captcha, *, *
p, *, *, POST, /api/verify-captcha, *, * p, *, *, POST, /api/verify-captcha, *, *
p, *, *, POST, /api/verify-code, *, *
p, *, *, POST, /api/reset-email-or-phone, *, * p, *, *, POST, /api/reset-email-or-phone, *, *
p, *, *, POST, /api/upload-resource, *, * p, *, *, POST, /api/upload-resource, *, *
p, *, *, GET, /.well-known/openid-configuration, *, * p, *, *, GET, /.well-known/openid-configuration, *, *

View File

@ -246,33 +246,14 @@ func (c *ApiController) Login() {
var msg string var msg string
if form.Password == "" { if form.Password == "" {
var verificationCodeType string
var checkResult string
if form.Name != "" {
user = object.GetUserByFields(form.Organization, form.Name)
}
// check result through Email or Phone
var checkDest string
if strings.Contains(form.Username, "@") {
verificationCodeType = "email"
if user != nil && util.GetMaskedEmail(user.Email) == form.Username {
form.Username = user.Email
}
checkDest = form.Username
} else {
verificationCodeType = "phone"
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.Username = user.Phone
}
}
if user = object.GetUserByFields(form.Organization, form.Username); user == nil { if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username))) c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
return return
} }
if verificationCodeType == "phone" {
verificationCodeType := object.GetVerifyType(form.Username)
var checkDest string
if verificationCodeType == object.VerifyTypePhone {
form.CountryCode = user.GetCountryCode(form.CountryCode) form.CountryCode = user.GetCountryCode(form.CountryCode)
var ok bool var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok { if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
@ -281,7 +262,8 @@ func (c *ApiController) Login() {
} }
} }
checkResult = object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage()) // check result through Email or Phone
checkResult := object.CheckSigninCode(user, checkDest, form.Code, c.GetAcceptLanguage())
if len(checkResult) != 0 { if len(checkResult) != 0 {
c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult)) c.ResponseError(fmt.Sprintf("%s - %s", verificationCodeType, checkResult))
return return

View File

@ -95,13 +95,13 @@ func (c *ApiController) GetUser() {
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
if owner == "" { if owner == "" {
owner, _ = util.GetOwnerAndNameFromId(id) owner = util.GetOwnerFromId(id)
} }
organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner)) organization := object.GetOrganization(fmt.Sprintf("%s/%s", "admin", owner))
if !organization.IsProfilePublic { if !organization.IsProfilePublic {
requestUserId := c.GetSessionUsername() requestUserId := c.GetSessionUsername()
hasPermission, err := object.CheckUserPermission(requestUserId, id, owner, false, c.GetAcceptLanguage()) hasPermission, err := object.CheckUserPermission(requestUserId, id, false, c.GetAcceptLanguage())
if !hasPermission { if !hasPermission {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -276,15 +276,35 @@ func (c *ApiController) SetPassword() {
userName := c.Ctx.Request.Form.Get("userName") userName := c.Ctx.Request.Form.Get("userName")
oldPassword := c.Ctx.Request.Form.Get("oldPassword") oldPassword := c.Ctx.Request.Form.Get("oldPassword")
newPassword := c.Ctx.Request.Form.Get("newPassword") newPassword := c.Ctx.Request.Form.Get("newPassword")
code := c.Ctx.Request.Form.Get("code")
if strings.Contains(newPassword, " ") {
c.ResponseError(c.T("user:New password cannot contain blank space."))
return
}
if len(newPassword) <= 5 {
c.ResponseError(c.T("user:New password must have at least 6 characters"))
return
}
requestUserId := c.GetSessionUsername()
userId := util.GetId(userOwner, userName) userId := util.GetId(userOwner, userName)
hasPermission, err := object.CheckUserPermission(requestUserId, userId, userOwner, true, c.GetAcceptLanguage()) requestUserId := c.GetSessionUsername()
if requestUserId == "" && code == "" {
return
} else if code == "" {
hasPermission, err := object.CheckUserPermission(requestUserId, userId, true, c.GetAcceptLanguage())
if !hasPermission { if !hasPermission {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
} else {
if code != c.GetSession("verifiedCode") {
c.ResponseError("")
return
}
c.SetSession("verifiedCode", "")
}
targetUser := object.GetUser(userId) targetUser := object.GetUser(userId)
@ -296,16 +316,6 @@ func (c *ApiController) SetPassword() {
} }
} }
if strings.Contains(newPassword, " ") {
c.ResponseError(c.T("user:New password cannot contain blank space."))
return
}
if len(newPassword) <= 5 {
c.ResponseError(c.T("user:New password must have at least 6 characters"))
return
}
targetUser.Password = newPassword targetUser.Password = newPassword
object.SetUserField(targetUser, "password", targetUser.Password) object.SetUserField(targetUser, "password", targetUser.Password)
c.ResponseOk() c.ResponseOk()

View File

@ -15,6 +15,7 @@
package controllers package controllers
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -110,7 +111,7 @@ func (c *ApiController) SendVerificationCode() {
sendResp := errors.New("invalid dest type") sendResp := errors.New("invalid dest type")
switch destType { switch destType {
case "email": case object.VerifyTypeEmail:
if !util.IsEmailValid(dest) { if !util.IsEmailValid(dest) {
c.ResponseError(c.T("check:Email is invalid")) c.ResponseError(c.T("check:Email is invalid"))
return return
@ -132,7 +133,7 @@ func (c *ApiController) SendVerificationCode() {
provider := application.GetEmailProvider() provider := application.GetEmailProvider()
sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest) sendResp = object.SendVerificationCodeToEmail(organization, user, provider, remoteAddr, dest)
case "phone": case object.VerifyTypePhone:
if method == LoginVerification || method == ForgetVerification { if method == LoginVerification || method == ForgetVerification {
if user != nil && util.GetMaskedPhone(user.Phone) == dest { if user != nil && util.GetMaskedPhone(user.Phone) == dest {
dest = user.Phone dest = user.Phone
@ -187,7 +188,7 @@ func (c *ApiController) ResetEmailOrPhone() {
checkDest := dest checkDest := dest
organization := object.GetOrganizationByUser(user) organization := object.GetOrganizationByUser(user)
if destType == "phone" { if destType == object.VerifyTypePhone {
if object.HasUserByField(user.Owner, "phone", dest) { if object.HasUserByField(user.Owner, "phone", dest) {
c.ResponseError(c.T("check:Phone already exists")) c.ResponseError(c.T("check:Phone already exists"))
return return
@ -207,7 +208,7 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), user.CountryCode)) c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), user.CountryCode))
return return
} }
} else if destType == "email" { } else if destType == object.VerifyTypeEmail {
if object.HasUserByField(user.Owner, "email", dest) { if object.HasUserByField(user.Owner, "email", dest) {
c.ResponseError(c.T("check:Email already exists")) c.ResponseError(c.T("check:Email already exists"))
return return
@ -230,10 +231,10 @@ func (c *ApiController) ResetEmailOrPhone() {
} }
switch destType { switch destType {
case "email": case object.VerifyTypeEmail:
user.Email = dest user.Email = dest
object.SetUserField(user, "email", user.Email) object.SetUserField(user, "email", user.Email)
case "phone": case object.VerifyTypePhone:
user.Phone = dest user.Phone = dest
object.SetUserField(user, "phone", user.Phone) object.SetUserField(user, "phone", user.Phone)
default: default:
@ -245,6 +246,60 @@ func (c *ApiController) ResetEmailOrPhone() {
c.ResponseOk() c.ResponseOk()
} }
// VerifyCode
// @Tag Account API
// @Title VerifyCode
// @router /api/verify-code [post]
func (c *ApiController) VerifyCode() {
var form RequestForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
c.ResponseError(err.Error())
return
}
var user *object.User
if form.Name != "" {
user = object.GetUserByFields(form.Organization, form.Name)
}
var checkDest string
if strings.Contains(form.Username, "@") {
if user != nil && util.GetMaskedEmail(user.Email) == form.Username {
form.Username = user.Email
}
checkDest = form.Username
} else {
if user != nil && util.GetMaskedPhone(user.Phone) == form.Username {
form.Username = user.Phone
}
}
if user = object.GetUserByFields(form.Organization, form.Username); user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), util.GetId(form.Organization, form.Username)))
return
}
verificationCodeType := object.GetVerifyType(form.Username)
if verificationCodeType == object.VerifyTypePhone {
form.CountryCode = user.GetCountryCode(form.CountryCode)
var ok bool
if checkDest, ok = util.GetE164Number(form.Username, form.CountryCode); !ok {
c.ResponseError(fmt.Sprintf(c.T("verification:Phone number is invalid in your region %s"), form.CountryCode))
return
}
}
if result := object.CheckVerificationCode(checkDest, form.Code, c.GetAcceptLanguage()); result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
}
object.DisableVerificationCode(checkDest)
c.SetSession("verifiedCode", form.Code)
c.ResponseOk()
}
// VerifyCaptcha ... // VerifyCaptcha ...
// @Title VerifyCaptcha // @Title VerifyCaptcha
// @Tag Verification API // @Tag Verification API

View File

@ -85,7 +85,7 @@ func GetFilteredUsers(m *ldap.Message) (filteredUsers []*object.User, code int)
return nil, ldap.LDAPResultInsufficientAccessRights return nil, ldap.LDAPResultInsufficientAccessRights
} }
} else { } else {
hasPermission, err := object.CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), org, true, "en") hasPermission, err := object.CheckUserPermission(fmt.Sprintf("%s/%s", m.Client.OrgName, m.Client.UserName), fmt.Sprintf("%s/%s", org, name), true, "en")
if !hasPermission { if !hasPermission {
log.Printf("ErrMsg = %v", err.Error()) log.Printf("ErrMsg = %v", err.Error())
return nil, ldap.LDAPResultInsufficientAccessRights return nil, ldap.LDAPResultInsufficientAccessRights

View File

@ -250,11 +250,13 @@ func filterField(field string) bool {
return reFieldWhiteList.MatchString(field) return reFieldWhiteList.MatchString(field)
} }
func CheckUserPermission(requestUserId, userId, userOwner string, strict bool, lang string) (bool, error) { func CheckUserPermission(requestUserId, userId string, strict bool, lang string) (bool, error) {
if requestUserId == "" { if requestUserId == "" {
return false, fmt.Errorf(i18n.Translate(lang, "general:Please login first")) return false, fmt.Errorf(i18n.Translate(lang, "general:Please login first"))
} }
userOwner := util.GetOwnerFromId(userId)
if userId != "" { if userId != "" {
targetUser := GetUser(userId) targetUser := GetUser(userId)
if targetUser == nil { if targetUser == nil {

View File

@ -18,6 +18,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
"time" "time"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
@ -38,6 +39,11 @@ const (
timeoutError = 3 timeoutError = 3
) )
const (
VerifyTypePhone = "phone"
VerifyTypeEmail = "email"
)
type VerificationRecord struct { type VerificationRecord struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
@ -213,6 +219,14 @@ func CheckSigninCode(user *User, dest, code, lang string) string {
} }
} }
func GetVerifyType(username string) (verificationCodeType string) {
if strings.Contains(username, "@") {
return VerifyTypeEmail
} else {
return VerifyTypeEmail
}
}
// From Casnode/object/validateCode.go line 116 // From Casnode/object/validateCode.go line 116
var stdNums = []byte("0123456789") var stdNums = []byte("0123456789")

View File

@ -115,6 +115,7 @@ func initAPI() {
beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword")
beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone") beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone")
beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode") beego.Router("/api/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
beego.Router("/api/verify-code", &controllers.ApiController{}, "POST:VerifyCode")
beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha") beego.Router("/api/verify-captcha", &controllers.ApiController{}, "POST:VerifyCaptcha")
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone") beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha") beego.Router("/api/get-captcha", &controllers.ApiController{}, "GET:GetCaptcha")

View File

@ -95,6 +95,15 @@ func GetOwnerAndNameFromId(id string) (string, string) {
return tokens[0], tokens[1] return tokens[0], tokens[1]
} }
func GetOwnerFromId(id string) string {
tokens := strings.Split(id, "/")
if len(tokens) != 2 {
panic(errors.New("GetOwnerAndNameFromId() error, wrong token count for ID: " + id))
}
return tokens[0]
}
func GetOwnerAndNameFromIdNoCheck(id string) (string, string) { func GetOwnerAndNameFromIdNoCheck(id string) (string, string) {
tokens := strings.SplitN(id, "/", 2) tokens := strings.SplitN(id, "/", 2)
return tokens[0], tokens[1] return tokens[0], tokens[1]

View File

@ -33,9 +33,8 @@ class ForgetPage extends React.Component {
classes: props, classes: props,
applicationName: props.applicationName ?? props.match.params?.applicationName, applicationName: props.applicationName ?? props.match.params?.applicationName,
msg: null, msg: null,
userId: "",
username: "",
name: "", name: "",
username: "",
phone: "", phone: "",
email: "", email: "",
dest: "", dest: "",
@ -86,7 +85,7 @@ class ForgetPage extends React.Component {
const phone = res.data.phone; const phone = res.data.phone;
const email = res.data.email; const email = res.data.email;
if (phone === "" && email === "") { if (!phone && !email) {
Setting.showMessage("error", "no verification method!"); Setting.showMessage("error", "no verification method!");
} else { } else {
this.setState({ this.setState({
@ -124,18 +123,16 @@ class ForgetPage extends React.Component {
}); });
break; break;
case "step2": case "step2":
const oAuthParams = Util.getOAuthGetParameters(); UserBackend.verifyCode({
AuthBackend.login({
application: forms.step2.getFieldValue("application"), application: forms.step2.getFieldValue("application"),
organization: forms.step2.getFieldValue("organization"), organization: forms.step2.getFieldValue("organization"),
username: forms.step2.getFieldValue("dest"), username: forms.step2.getFieldValue("dest"),
name: this.state.name, name: this.state.name,
code: forms.step2.getFieldValue("code"), code: forms.step2.getFieldValue("code"),
type: "login", type: "login",
}, oAuthParams).then(res => { }).then(res => {
if (res.status === "ok") { if (res.status === "ok") {
this.setState({current: 2, userId: res.data}); this.setState({current: 2, code: forms.step2.getFieldValue("code")});
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
} }
@ -150,7 +147,7 @@ class ForgetPage extends React.Component {
onFinish(values) { onFinish(values) {
values.username = this.state.name; values.username = this.state.name;
values.userOwner = this.getApplicationObj()?.organizationObj.name; values.userOwner = this.getApplicationObj()?.organizationObj.name;
UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword).then(res => { UserBackend.setPassword(values.userOwner, values.username, "", values?.newPassword, this.state.code).then(res => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history); Setting.redirectToLoginPage(this.getApplicationObj(), this.props.history);
} else { } else {
@ -387,7 +384,6 @@ class ForgetPage extends React.Component {
hasFeedback hasFeedback
> >
<Input.Password <Input.Password
disabled={this.state.userId === ""}
prefix={<LockOutlined />} prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")} placeholder={i18next.t("general:Password")}
/> />
@ -414,14 +410,13 @@ class ForgetPage extends React.Component {
]} ]}
> >
<Input.Password <Input.Password
disabled={this.state.userId === ""}
prefix={<CheckCircleOutlined />} prefix={<CheckCircleOutlined />}
placeholder={i18next.t("signup:Confirm")} placeholder={i18next.t("signup:Confirm")}
/> />
</Form.Item> </Form.Item>
<br /> <br />
<Form.Item hidden={this.state.current !== 2}> <Form.Item hidden={this.state.current !== 2}>
<Button block type="primary" htmlType="submit" disabled={this.state.userId === ""}> <Button block type="primary" htmlType="submit">
{i18next.t("forget:Change Password")} {i18next.t("forget:Change Password")}
</Button> </Button>
</Form.Item> </Form.Item>

View File

@ -93,12 +93,16 @@ export function getAffiliationOptions(url, code) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function setPassword(userOwner, userName, oldPassword, newPassword) { export function setPassword(userOwner, userName, oldPassword, newPassword, code = "") {
const formData = new FormData(); const formData = new FormData();
formData.append("userOwner", userOwner); formData.append("userOwner", userOwner);
formData.append("userName", userName); formData.append("userName", userName);
formData.append("oldPassword", oldPassword); formData.append("oldPassword", oldPassword);
formData.append("newPassword", newPassword); formData.append("newPassword", newPassword);
if (code) {
formData.append("code", code);
}
return fetch(`${Setting.ServerUrl}/api/set-password`, { return fetch(`${Setting.ServerUrl}/api/set-password`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
@ -188,3 +192,14 @@ export function getCaptcha(owner, name, isCurrentProvider) {
}, },
}).then(res => res.json()).then(res => res.data); }).then(res => res.json()).then(res => res.data);
} }
export function verifyCode(values) {
return fetch(`${Setting.ServerUrl}/api/verify-code`, {
method: "POST",
credentials: "include",
body: JSON.stringify(values),
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}