feat: check user email and phone when signing up

Signed-off-by: Kininaru <shiftregister233@outlook.com>

phone prefix error

Signed-off-by: Kininaru <shiftregister233@outlook.com>

fix i18n

Signed-off-by: Kininaru <shiftregister233@outlook.com>

fix i18n error

Signed-off-by: Kininaru <shiftregister233@outlook.com>

removed useless file

Signed-off-by: Kininaru <shiftregister233@outlook.com>

move timeout to app.conf

Signed-off-by: Kininaru <shiftregister233@outlook.com>

i18n

Signed-off-by: Kininaru <shiftregister233@outlook.com>

made verification code reusable

Signed-off-by: Kininaru <shiftregister233@outlook.com>
This commit is contained in:
Kininaru 2021-05-18 20:11:03 +08:00
parent 9bc29e25ef
commit 66d953a6c1
14 changed files with 151 additions and 37 deletions

View File

@ -86,6 +86,7 @@ p, *, *, GET, /api/get-default-providers, *, *
p, *, *, POST, /api/upload-avatar, *, * p, *, *, POST, /api/upload-avatar, *, *
p, *, *, POST, /api/unlink, *, * p, *, *, POST, /api/unlink, *, *
p, *, *, POST, /api/set-password, *, * p, *, *, POST, /api/set-password, *, *
p, *, *, POST, /api/send-verification-code, *, *
` `
sa := stringadapter.NewAdapter(ruleText) sa := stringadapter.NewAdapter(ruleText)

View File

@ -7,4 +7,5 @@ driverName = mysql
dataSourceName = root:123@tcp(localhost:3306)/ dataSourceName = root:123@tcp(localhost:3306)/
dbName = casdoor dbName = casdoor
authState = "casdoor" authState = "casdoor"
useProxy = false useProxy = false
verificationCodeTimeout = 10

View File

@ -46,6 +46,10 @@ type RequestForm struct {
State string `json:"state"` State string `json:"state"`
RedirectUri string `json:"redirectUri"` RedirectUri string `json:"redirectUri"`
Method string `json:"method"` Method string `json:"method"`
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`
PhonePrefix string `json:"phonePrefix"`
} }
type Response struct { type Response struct {
@ -77,6 +81,21 @@ func (c *ApiController) Signup() {
panic(err) panic(err)
} }
checkResult := object.CheckVerificationCode(form.Email, form.EmailCode)
if len(checkResult) != 0 {
responseText := fmt.Sprintf("Email%s", checkResult)
c.ResponseError(responseText)
return
}
checkPhone := fmt.Sprintf("+%s%s", form.PhonePrefix, form.Phone)
checkResult = object.CheckVerificationCode(checkPhone, form.PhoneCode)
if len(checkResult) != 0 {
responseText := fmt.Sprintf("Phone%s", checkResult)
c.ResponseError(responseText)
return
}
application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application)) application := object.GetApplication(fmt.Sprintf("admin/%s", form.Application))
if !application.EnableSignUp { if !application.EnableSignUp {
resp = Response{Status: "error", Msg: "The application does not allow to sign up new account", Data: c.GetSessionUser()} resp = Response{Status: "error", Msg: "The application does not allow to sign up new account", Data: c.GetSessionUser()}
@ -110,6 +129,8 @@ func (c *ApiController) Signup() {
//c.SetSessionUser(user) //c.SetSessionUser(user)
object.DisableVerificationCode(form.Email)
object.DisableVerificationCode(checkPhone)
util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId) util.LogInfo(c.Ctx, "API: [%s] is signed up as new user", userId)
resp = Response{Status: "ok", Msg: "", Data: userId} resp = Response{Status: "ok", Msg: "", Data: userId}
} }

View File

@ -23,25 +23,14 @@ import (
) )
func (c *ApiController) SendVerificationCode() { func (c *ApiController) SendVerificationCode() {
userId, ok := c.RequireSignedIn()
if !ok {
return
}
user := object.GetUser(userId)
if user == nil {
c.ResponseError("No such user.")
return
}
destType := c.Ctx.Request.Form.Get("type") destType := c.Ctx.Request.Form.Get("type")
dest := c.Ctx.Request.Form.Get("dest") dest := c.Ctx.Request.Form.Get("dest")
orgId := c.Ctx.Request.Form.Get("organizationId")
remoteAddr := c.Ctx.Request.RemoteAddr remoteAddr := c.Ctx.Request.RemoteAddr
remoteAddr = remoteAddr[:strings.LastIndex(remoteAddr, ":")] remoteAddr = remoteAddr[:strings.LastIndex(remoteAddr, ":")]
if len(destType) == 0 || len(dest) == 0 { if len(destType) == 0 || len(dest) == 0 || len(orgId) == 0 || strings.Index(orgId, "/") < 0 {
c.Data["json"] = Response{Status: "error", Msg: "Missing parameter."} c.ResponseError("Missing parameter.")
c.ServeJSON()
return return
} }
@ -58,12 +47,12 @@ func (c *ApiController) SendVerificationCode() {
c.ResponseError("Invalid phone number") c.ResponseError("Invalid phone number")
return return
} }
org := object.GetOrganizationByUser(user) org := object.GetOrganization(orgId)
phonePrefix := "86" if org == nil {
if org != nil && org.PhonePrefix != "" { c.ResponseError("Missing parameter.")
phonePrefix = org.PhonePrefix return
} }
dest = fmt.Sprintf("+%s%s", phonePrefix, dest) dest = fmt.Sprintf("+%s%s", org.PhonePrefix, dest)
msg = object.SendVerificationCodeToPhone(remoteAddr, dest) msg = object.SendVerificationCodeToPhone(remoteAddr, dest)
} }
@ -122,6 +111,7 @@ func (c *ApiController) ResetEmailOrPhone() {
return return
} }
object.DisableVerificationCode(checkDest)
c.Data["json"] = Response{Status: "ok"} c.Data["json"] = Response{Status: "ok"}
c.ServeJSON() c.ServeJSON()
} }

View File

@ -36,6 +36,7 @@ type Provider struct {
RegionId string `xorm:"varchar(100)" json:"regionId"` RegionId string `xorm:"varchar(100)" json:"regionId"`
SignName string `xorm:"varchar(100)" json:"signName"` SignName string `xorm:"varchar(100)" json:"signName"`
TemplateCode string `xorm:"varchar(100)" json:"templateCode"` TemplateCode string `xorm:"varchar(100)" json:"templateCode"`
AppId string `xorm:"varchar(100)" json:"appId"`
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"` ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"`
} }

View File

@ -25,7 +25,7 @@ func SendCodeToPhone(phone, code string) string {
if provider == nil { if provider == nil {
return "Please set an phone provider first" return "Please set an phone provider first"
} }
client := go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.RegionId, provider.TemplateCode) client := go_sms_sender.NewSmsClient(provider.Type, provider.ClientId, provider.ClientSecret, provider.SignName, provider.RegionId, provider.TemplateCode, provider.AppId)
if client == nil { if client == nil {
return fmt.Sprintf("Unsupported provide type: %s", provider.Type) return fmt.Sprintf("Unsupported provide type: %s", provider.Type)
} }

View File

@ -19,6 +19,7 @@ import (
"math/rand" "math/rand"
"time" "time"
"github.com/astaxie/beego"
"xorm.io/core" "xorm.io/core"
) )
@ -94,34 +95,54 @@ func AddToVerificationRecord(remoteAddr, recordType, dest, code string) string {
return "" return ""
} }
func CheckVerificationCode(dest, code string) string { func getVerificationRecord(dest string) *VerificationRecord {
var record VerificationRecord var record VerificationRecord
record.Receiver = dest record.Receiver = dest
has, err := adapter.Engine.Desc("time").Where("is_used = 0").Get(&record) has, err := adapter.Engine.Desc("time").Where("is_used = 0").Get(&record)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if !has { if !has {
return nil
}
return &record
}
func CheckVerificationCode(dest, code string) string {
record := getVerificationRecord(dest)
if record == nil {
return "Code has not been sent yet!" return "Code has not been sent yet!"
} }
timeout, err := beego.AppConfig.Int64("verificationCodeTimeout")
if err != nil {
panic(err)
}
now := time.Now().Unix() now := time.Now().Unix()
if now-record.Time > 5*60 { if now-record.Time > timeout*60 {
return "You should verify your code in 5 min!" return fmt.Sprintf("You should verify your code in %d min!", timeout)
} }
if record.Code != code { if record.Code != code {
return "Wrong code!" return "Wrong code!"
} }
return ""
}
func DisableVerificationCode(dest string) {
record := getVerificationRecord(dest)
if record == nil {
return
}
record.IsUsed = true record.IsUsed = true
_, err = adapter.Engine.ID(core.PK{record.RemoteAddr, record.Type}).AllCols().Update(record) _, err := adapter.Engine.ID(core.PK{record.RemoteAddr, record.Type}).AllCols().Update(record)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return ""
} }
// from Casnode/object/validateCode.go line 116 // from Casnode/object/validateCode.go line 116

View File

@ -222,6 +222,18 @@ class ProviderEditPage extends React.Component {
</React.Fragment> </React.Fragment>
) : null ) : null
} }
{this.state.provider.category === "Phone" && this.state.provider.type === "tencent" ? (
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}>
{i18next.t("provider:App ID")}:
</Col>
<Col span={22} >
<Input value={this.state.provider.appId} onChange={e => {
this.updateProviderField('appId', e.target.value);
}} />
</Col>
</Row>
) : null}
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={2}> <Col style={{marginTop: '5px'}} span={2}>
{i18next.t("provider:Provider URL")}: {i18next.t("provider:Provider URL")}:

View File

@ -25,7 +25,7 @@ export const ResetModal = (props) => {
const [sendCodeCoolDown, setCoolDown] = React.useState(false); const [sendCodeCoolDown, setCoolDown] = React.useState(false);
const [dest, setDest] = React.useState(""); const [dest, setDest] = React.useState("");
const [code, setCode] = React.useState(""); const [code, setCode] = React.useState("");
const {buttonText, destType, coolDownTime} = props; const {buttonText, destType, coolDownTime, org} = props;
const showModal = () => { const showModal = () => {
setVisible(true); setVisible(true);
@ -72,7 +72,8 @@ export const ResetModal = (props) => {
Setting.showMessage("error", i18next.t("user:Empty " + destType)); Setting.showMessage("error", i18next.t("user:Empty " + destType));
return; return;
} }
UserBackend.sendCode(dest, destType).then(res => { let orgId = org.owner + "/" + org.name;
UserBackend.sendCode(dest, destType, orgId).then(res => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("user:Code Sent")); Setting.showMessage("success", i18next.t("user:Code Sent"));
setCoolDown(true); setCoolDown(true);

View File

@ -267,7 +267,7 @@ class UserEditPage extends React.Component {
<Input value={this.state.user.email} disabled /> <Input value={this.state.user.email} disabled />
</Col> </Col>
<Col span={11} > <Col span={11} >
{ this.state.user.id === this.props.account.id ? (<ResetModal buttonText={i18next.t("user:Reset Email")} destType={"email"} coolDownTime={60}/>) : null} { this.state.user.id === this.props.account.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Email")} destType={"email"} coolDownTime={60}/>) : null}
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >
@ -278,7 +278,7 @@ class UserEditPage extends React.Component {
<Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`} disabled /> <Input value={this.state.user.phone} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`} disabled />
</Col> </Col>
<Col span={11} > <Col span={11} >
{ this.state.user.id === this.props.account.id ? (<ResetModal buttonText={i18next.t("user:Reset Phone")} destType={"phone"} coolDownTime={60}/>) : null} { this.state.user.id === this.props.account.id ? (<ResetModal org={this.state.application?.organizationObj} buttonText={i18next.t("user:Reset Phone")} destType={"phone"} coolDownTime={60}/>) : null}
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: '20px'}} > <Row style={{marginTop: '20px'}} >

View File

@ -21,6 +21,7 @@ import i18next from "i18next";
import * as Util from "./Util"; import * as Util from "./Util";
import {authConfig} from "./Auth"; import {authConfig} from "./Auth";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as UserBackend from "../backend/UserBackend";
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
@ -61,6 +62,8 @@ class SignupPage extends React.Component {
classes: props, classes: props,
applicationName: props.match.params.applicationName !== undefined ? props.match.params.applicationName : authConfig.appName, applicationName: props.match.params.applicationName !== undefined ? props.match.params.applicationName : authConfig.appName,
application: null, application: null,
email: "",
phone: ""
}; };
this.form = React.createRef(); this.form = React.createRef();
@ -96,12 +99,13 @@ class SignupPage extends React.Component {
} }
onFinish(values) { onFinish(values) {
values.phonePrefix = this.state.application?.organizationObj.phonePrefix;
AuthBackend.signup(values) AuthBackend.signup(values)
.then((res) => { .then((res) => {
if (res.status === 'ok') { if (res.status === 'ok') {
Setting.goToLinkSoft(this, this.getResultPath(this.state.application)); Setting.goToLinkSoft(this, this.getResultPath(this.state.application));
} else { } else {
Setting.showMessage("error", `Failed to sign up: ${res.msg}`); Setting.showMessage("error", i18next.t(`signup:${res.msg}`));
} }
}); });
} }
@ -110,6 +114,22 @@ class SignupPage extends React.Component {
this.form.current.scrollToField(errorFields[0].name); this.form.current.scrollToField(errorFields[0].name);
} }
sendCode(type) {
let dest, orgId;
if (type === "email") {
dest = this.state.email;
} else if (type === "phone") {
dest = this.state.phone;
} else return;
orgId = this.state.application?.organizationObj.owner + "/" + this.state.application?.organizationObj.name
UserBackend.sendCode(dest, type, orgId).then(res => {
if (res.status === "ok") Setting.showMessage("success", i18next.t("signup:code sent"));
else Setting.showMessage("error", i18next.t("signup:" + res.msg));
})
}
renderForm(application) { renderForm(application) {
if (!application.enableSignUp) { if (!application.enableSignUp) {
return ( return (
@ -220,7 +240,17 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input /> <Input onChange={e => this.setState({email: e.target.value})} />
</Form.Item>
<Form.Item
name="emailCode"
label={i18next.t("signup:email code")}
rules={[{
required: true,
message: i18next.t("signup:Please input your verification code!"),
}]}
>
<Input autoComplete="off" value={this.state.emailCode} addonAfter={<button onClick={() => this.sendCode("email")} style={{backgroundColor: "#fafafa", border: "none"}}>{i18next.t("signup:send code")}</button>} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="password" name="password"
@ -273,8 +303,21 @@ class SignupPage extends React.Component {
width: '100%', width: '100%',
}} }}
addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`} addonBefore={`+${this.state.application?.organizationObj.phonePrefix}`}
onChange={e => this.setState({phone: e.target.value})}
/> />
</Form.Item> </Form.Item>
<Form.Item
name="phoneCode"
label={i18next.t("signup:phone code")}
rules={[
{
required: true,
message: i18next.t("signup:Please input your phone verification code!"),
},
]}
>
<Input autoComplete="off" value={this.state.phoneCode} addonAfter={<button onClick={() => this.sendCode("phone")} style={{border: "none", backgroundColor: "#fafafa"}}>{i18next.t("signup:send code")}</button>}/>
</Form.Item>
<Form.Item name="agreement" valuePropName="checked" {...tailFormItemLayout}> <Form.Item name="agreement" valuePropName="checked" {...tailFormItemLayout}>
<Checkbox> <Checkbox>
{i18next.t("signup:Accept")}&nbsp; {i18next.t("signup:Accept")}&nbsp;

View File

@ -93,10 +93,11 @@ export function setPassword(userOwner, userName, oldPassword, newPassword) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function sendCode(dest, type) { export function sendCode(dest, type, orgId) {
let formData = new FormData(); let formData = new FormData();
formData.append("dest", dest); formData.append("dest", dest);
formData.append("type", type); formData.append("type", type);
formData.append("organizationId", orgId);
return fetch(`${Setting.ServerUrl}/api/send-verification-code`, { return fetch(`${Setting.ServerUrl}/api/send-verification-code`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",

View File

@ -48,7 +48,18 @@
"Have account?": "Have account?", "Have account?": "Have account?",
"sign in now": "sign in now", "sign in now": "sign in now",
"Your account has been created!": "Your account has been created!", "Your account has been created!": "Your account has been created!",
"Please click the below button to sign in": "Please click the below button to sign in" "Please click the below button to sign in": "Please click the below button to sign in",
"code sent": "code sent",
"send code": "send code",
"email code": "email code",
"phone code": "phone code",
"PhoneCode has not been sent yet!": "Phone code has not been sent yet!",
"EmailCode has not been sent yet!": "Email code has not been sent yet!",
"PhoneYou should verify your code in 10 min!": "You should verify your phone verification code in 10 min!",
"EmailYou should verify your code in 10 min!": "You should verify your email verification code in 10 min!",
"PhoneWrong code!": "Wrong phone verification code!",
"EmailWrong code!": "Wrong email verification code!",
"Missing parameter.": "Missing parameter."
}, },
"login": "login":
{ {

View File

@ -48,7 +48,18 @@
"Have account?": "已有账号?", "Have account?": "已有账号?",
"sign in now": "立即登录", "sign in now": "立即登录",
"Your account has been created!": "您的账号已创建!", "Your account has been created!": "您的账号已创建!",
"Please click the below button to sign in": "请点击下方按钮登录" "Please click the below button to sign in": "请点击下方按钮登录",
"code sent": "验证码已发送",
"send code": "发送验证码",
"email code": "邮箱验证码",
"phone code": "手机验证码",
"PhoneCode has not been sent yet!": "尚未发送验证码至手机",
"EmailCode has not been sent yet!": "尚未发送验证码至邮箱",
"PhoneYou should verify your code in 10 min!": "你应该在 10 分钟之内验证手机号",
"EmailYou should verify your code in 10 min!": "你应该在 10 分钟之内验证邮箱",
"PhoneWrong code!": "手机验证码错误",
"EmailWrong code!": "邮箱验证码错误",
"Missing parameter.": "缺少参数"
}, },
"login": "login":
{ {