mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 10:45:47 +08:00
Merge pull request #55 from Kininaru/master
feat: add reset Email and phone by using verification code.
This commit is contained in:
commit
e4b6f758a7
@ -12,3 +12,10 @@ mailUser = ""
|
||||
mailPass = ""
|
||||
mailHost = ""
|
||||
mailPort = ""
|
||||
smsProvider = ""
|
||||
smsAccessId = ""
|
||||
smsAccessKey = ""
|
||||
smsAppId = ""
|
||||
smsSign = ""
|
||||
smsRegion = ""
|
||||
smsTemplateId = ""
|
@ -101,7 +101,6 @@ func (c *ApiController) Signup() {
|
||||
DisplayName: form.Name,
|
||||
Avatar: "https://casbin.org/img/casbin.svg",
|
||||
Email: form.Email,
|
||||
PhonePrefix: form.PhonePrefix,
|
||||
Phone: form.Phone,
|
||||
Affiliation: form.Affiliation,
|
||||
IsAdmin: false,
|
||||
|
129
controllers/verification.go
Normal file
129
controllers/verification.go
Normal file
@ -0,0 +1,129 @@
|
||||
// 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.
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (c *ApiController) SendVerificationCode() {
|
||||
userId := c.GetSessionUser()
|
||||
if len(userId) == 0 {
|
||||
c.ResponseError("Please sign in first")
|
||||
return
|
||||
}
|
||||
user := object.GetUser(userId)
|
||||
if user == nil {
|
||||
c.ResponseError("No such user.")
|
||||
return
|
||||
}
|
||||
|
||||
destType := c.Ctx.Request.Form.Get("type")
|
||||
dest := c.Ctx.Request.Form.Get("dest")
|
||||
remoteAddr := c.Ctx.Request.RemoteAddr
|
||||
remoteAddr = remoteAddr[:strings.LastIndex(remoteAddr, ":")]
|
||||
|
||||
if len(destType) == 0 || len(dest) == 0 {
|
||||
c.Data["json"] = Response{Status: "error", Msg: "Missing parameter."}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
ret := "Invalid dest type."
|
||||
switch destType {
|
||||
case "email":
|
||||
if !util.IsEmailValid(dest) {
|
||||
c.ResponseError("Invalid Email address")
|
||||
return
|
||||
}
|
||||
ret = object.SendVerificationCodeToEmail(remoteAddr, dest)
|
||||
case "phone":
|
||||
if !util.IsPhoneCnValid(dest) {
|
||||
c.ResponseError("Invalid phone number")
|
||||
return
|
||||
}
|
||||
org := object.GetOrganizationByName(user.Owner)
|
||||
phonePrefix := "86"
|
||||
if org != nil && org.PhonePrefix != "" {
|
||||
phonePrefix = org.PhonePrefix
|
||||
}
|
||||
dest = fmt.Sprintf("+%s%s", phonePrefix, dest)
|
||||
ret = object.SendVerificationCodeToPhone(remoteAddr, dest)
|
||||
}
|
||||
|
||||
var status string
|
||||
if len(ret) == 0 {
|
||||
status = "ok"
|
||||
} else {
|
||||
status = "error"
|
||||
}
|
||||
|
||||
c.Data["json"] = Response{Status: status, Msg: ret}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *ApiController) ResetEmailOrPhone() {
|
||||
userId := c.GetSessionUser()
|
||||
if len(userId) == 0 {
|
||||
c.ResponseError("Please sign in first")
|
||||
return
|
||||
}
|
||||
user := object.GetUser(userId)
|
||||
if user == nil {
|
||||
c.ResponseError("No such user.")
|
||||
return
|
||||
}
|
||||
|
||||
destType := c.Ctx.Request.Form.Get("type")
|
||||
dest := c.Ctx.Request.Form.Get("dest")
|
||||
code := c.Ctx.Request.Form.Get("code")
|
||||
if len(dest) == 0 || len(code) == 0 || len(destType) == 0 {
|
||||
c.ResponseError("Missing parameter.")
|
||||
return
|
||||
}
|
||||
|
||||
checkDest := dest
|
||||
if destType == "phone" {
|
||||
org := object.GetOrganizationByName(user.Owner)
|
||||
phonePrefix := "86"
|
||||
if org != nil && org.PhonePrefix != "" {
|
||||
phonePrefix = org.PhonePrefix
|
||||
}
|
||||
checkDest = fmt.Sprintf("+%s%s", phonePrefix, dest)
|
||||
}
|
||||
if ret := object.CheckVerificationCode(checkDest, code); len(ret) != 0 {
|
||||
c.ResponseError(ret)
|
||||
return
|
||||
}
|
||||
|
||||
switch destType {
|
||||
case "email":
|
||||
user.Email = dest
|
||||
object.SetUserField(user, "email", user.Email)
|
||||
case "phone":
|
||||
user.Phone = dest
|
||||
object.SetUserField(user, "phone", user.Phone)
|
||||
default:
|
||||
c.ResponseError("Unknown type.")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = Response{Status: "ok"}
|
||||
c.ServeJSON()
|
||||
}
|
3
go.mod
3
go.mod
@ -9,6 +9,7 @@ require (
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
|
||||
github.com/casbin/casbin/v2 v2.23.4
|
||||
github.com/casbin/xorm-adapter/v2 v2.2.0
|
||||
github.com/casdoor/go-sms-sender v0.0.1
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
||||
@ -24,6 +25,8 @@ require (
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
xorm.io/core v0.7.2
|
||||
xorm.io/xorm v1.0.3
|
||||
|
18
go.sum
18
go.sum
@ -13,6 +13,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1075 h1:Z0SzZttfYI/raZ5O9WF3cezZJTSW4Yz4Kow9uWdyRwg=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1075/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible h1:Ft+KeWIJxFP76LqgJbvtOA1qBIoC8vGkTV3QeCOeJC4=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
@ -37,6 +39,8 @@ github.com/casbin/casbin/v2 v2.23.4 h1:izvAG3KA49C3/m1zpYfkLcZlkYQO5VeHj7dhwurwZ
|
||||
github.com/casbin/casbin/v2 v2.23.4/go.mod h1:wUgota0cQbTXE6Vd+KWpg41726jFRi7upxio0sR+Xd0=
|
||||
github.com/casbin/xorm-adapter/v2 v2.2.0 h1:wAuYpCDRPUSFxdRqcRrGS0664UC7RKE21x7wrIl3rLQ=
|
||||
github.com/casbin/xorm-adapter/v2 v2.2.0/go.mod h1:9bPGOgjA/qbtjXHt3FC1rRyyRMkt9c+m8vlfLWjdSXU=
|
||||
github.com/casdoor/go-sms-sender v0.0.1 h1:n/r6fGgXsV+6uMxXvb0XLZnUCjmbUB1uSB817Ej0/gI=
|
||||
github.com/casdoor/go-sms-sender v0.0.1/go.mod h1:rr4na8Zc+0vgPVY5JPB0LZkRVuj5AhNVhE1G7W8lDk8=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||
@ -71,6 +75,7 @@ github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gG
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
@ -103,11 +108,14 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
|
||||
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
@ -132,8 +140,10 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
|
||||
github.com/mileusna/crontab v1.0.1 h1:YrDLc7l3xOiznmXq2FtAgg+1YQ3yC6pfFVPe+ywXNtg=
|
||||
github.com/mileusna/crontab v1.0.1/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4U1ZwExmc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
@ -183,6 +193,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
|
||||
@ -197,6 +208,8 @@ github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2K
|
||||
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v1.0.154 h1:THBgwGwUQtsw6L53cSSA2wwL3sLrm+HJ3Dk+ye/lMCI=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go v1.0.154/go.mod h1:asUz5BPXxgoPGaRgZaVm1iGcUAuHyYUo1nXqKa83cvI=
|
||||
github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo=
|
||||
github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
|
||||
github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||
@ -262,12 +275,17 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
|
@ -128,4 +128,9 @@ func (a *Adapter) createTable() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(VerificationRecord))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ type Organization struct {
|
||||
Favicon string `xorm:"varchar(100)" json:"favicon"`
|
||||
PasswordType string `xorm:"varchar(100)" json:"passwordType"`
|
||||
PasswordSalt string `xorm:"varchar(100)" json:"passwordSalt"`
|
||||
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
|
||||
}
|
||||
|
||||
func GetOrganizations(owner string) []*Organization {
|
||||
@ -91,3 +92,16 @@ func DeleteOrganization(organization *Organization) bool {
|
||||
|
||||
return affected != 0
|
||||
}
|
||||
|
||||
func GetOrganizationByName(name string) *Organization {
|
||||
var ret Organization
|
||||
ret.Name = name
|
||||
has, err := adapter.Engine.Get(&ret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !has {
|
||||
return nil
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
53
object/sms.go
Normal file
53
object/sms.go
Normal file
@ -0,0 +1,53 @@
|
||||
// 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.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/casdoor/go-sms-sender"
|
||||
)
|
||||
|
||||
var client go_sms_sender.SmsClient
|
||||
var provider string
|
||||
|
||||
func InitSmsClient() {
|
||||
provider = beego.AppConfig.String("smsProvider")
|
||||
accessId := beego.AppConfig.String("smsAccessId")
|
||||
accessKey := beego.AppConfig.String("smsAccessKey")
|
||||
appId := beego.AppConfig.String("smsAppId")
|
||||
sign := beego.AppConfig.String("smsSign")
|
||||
region := beego.AppConfig.String("smsRegion")
|
||||
templateId := beego.AppConfig.String("smsTemplateId")
|
||||
client = go_sms_sender.NewSmsClient(provider, accessId, accessKey, sign, region, templateId, appId)
|
||||
}
|
||||
|
||||
func SendCodeToPhone(phone, code string) {
|
||||
if client == nil {
|
||||
InitSmsClient()
|
||||
if client == nil {
|
||||
fmt.Println("Sms Config Error")
|
||||
return
|
||||
}
|
||||
}
|
||||
param := make(map[string]string)
|
||||
if provider == "tencent" {
|
||||
param["0"] = code
|
||||
} else {
|
||||
param["code"] = code
|
||||
}
|
||||
client.SendMessage(param, phone)
|
||||
}
|
@ -34,7 +34,6 @@ type User struct {
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Avatar string `xorm:"varchar(255)" json:"avatar"`
|
||||
Email string `xorm:"varchar(100)" json:"email"`
|
||||
PhonePrefix string `xorm:"varchar(10)" json:"phonePrefix"`
|
||||
Phone string `xorm:"varchar(100)" json:"phone"`
|
||||
Affiliation string `xorm:"varchar(100)" json:"affiliation"`
|
||||
Tag string `xorm:"varchar(100)" json:"tag"`
|
||||
|
130
object/verification.go
Normal file
130
object/verification.go
Normal file
@ -0,0 +1,130 @@
|
||||
// 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.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VerificationRecord struct {
|
||||
RemoteAddr string `xorm:"varchar(100) notnull pk"`
|
||||
Receiver string `xorm:"varchar(100) notnull"`
|
||||
Code string `xorm:"varchar(10) notnull"`
|
||||
Time int64 `xorm:"notnull"`
|
||||
IsUsed bool
|
||||
}
|
||||
|
||||
func SendVerificationCodeToEmail(remoteAddr, dest string) string {
|
||||
title := "Casdoor Code"
|
||||
sender := "Casdoor Admin"
|
||||
code := getRandomCode(5)
|
||||
content := fmt.Sprintf("You have requested a verification code at Casdoor. Here is your code: %s, please enter in 5 minutes.", code)
|
||||
|
||||
if result := AddToVerificationRecord(remoteAddr, dest, code); len(result) != 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
if err := SendEmail(title, content, dest, sender); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func SendVerificationCodeToPhone(remoteAddr, dest string) string {
|
||||
code := getRandomCode(5)
|
||||
if result := AddToVerificationRecord(remoteAddr, dest, code); len(result) != 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
SendCodeToPhone(dest, code)
|
||||
return ""
|
||||
}
|
||||
|
||||
func AddToVerificationRecord(remoteAddr, dest, code string) string {
|
||||
var record VerificationRecord
|
||||
record.RemoteAddr = remoteAddr
|
||||
has, err := adapter.Engine.Get(&record)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
if has && now - record.Time < 60 {
|
||||
return "You can only send one code in 60s."
|
||||
}
|
||||
|
||||
record.Receiver = dest
|
||||
record.Code = code
|
||||
record.Time = now
|
||||
record.IsUsed = false
|
||||
|
||||
if has {
|
||||
_, err = adapter.Engine.ID(record.RemoteAddr).AllCols().Update(record)
|
||||
} else {
|
||||
_, err = adapter.Engine.Insert(record)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func CheckVerificationCode(dest, code string) string {
|
||||
var record VerificationRecord
|
||||
record.Receiver = dest
|
||||
has, err := adapter.Engine.Desc("time").Where("is_used = 0").Get(&record)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !has {
|
||||
return "Code has not been sent yet!"
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if now-record.Time > 5*60 {
|
||||
return "You should verify your code in 5 min!"
|
||||
}
|
||||
|
||||
if record.Code != code {
|
||||
return "Wrong code!"
|
||||
}
|
||||
|
||||
record.IsUsed = true
|
||||
_, err = adapter.Engine.ID(record.RemoteAddr).AllCols().Update(record)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// from Casnode/object/validateCode.go line 116
|
||||
var stdNums = []byte("0123456789")
|
||||
|
||||
func getRandomCode(length int) string {
|
||||
var result []byte
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for i := 0; i < length; i++ {
|
||||
result = append(result, stdNums[r.Intn(len(stdNums))])
|
||||
}
|
||||
return string(result)
|
||||
}
|
@ -60,6 +60,8 @@ 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/send-verification-code", &controllers.ApiController{}, "POST:SendVerificationCode")
|
||||
beego.Router("/api/reset-email-or-phone", &controllers.ApiController{}, "POST:ResetEmailOrPhone")
|
||||
|
||||
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders")
|
||||
beego.Router("/api/get-default-providers", &controllers.ApiController{}, "GET:GetDefaultProviders")
|
||||
|
@ -21,7 +21,7 @@ var rePhoneCn *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
reEmail, _ = regexp.Compile(`^[0-9a-z][_.0-9a-z-]{0,31}@([0-9a-z][0-9a-z-]{0,30}[0-9a-z]\.){1,4}[a-z]{2,4}$`)
|
||||
rePhoneCn, _ = regexp.Compile("^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$")
|
||||
rePhoneCn, _ = regexp.Compile("^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|191|198|199|(147))\\d{8}$")
|
||||
}
|
||||
|
||||
func IsEmailValid(email string) bool {
|
||||
|
@ -149,6 +149,16 @@ class OrganizationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
<Col style={{marginTop: '5px'}} span={2}>
|
||||
{i18next.t("general:Phone Prefix")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input addonBefore={"+"} value={this.state.organization.phonePrefix} onChange={e => {
|
||||
this.updateOrganizationField('phonePrefix', e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
122
web/src/ResetModal.js
Normal file
122
web/src/ResetModal.js
Normal file
@ -0,0 +1,122 @@
|
||||
// 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 {Button, Col, Modal, Row, Input,} from "antd";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import * as Setting from "./Setting"
|
||||
import * as UserBackend from "./backend/UserBackend"
|
||||
|
||||
export const ResetModal = (props) => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [confirmLoading, setConfirmLoading] = React.useState(false);
|
||||
const [sendButtonText, setSendButtonText] = React.useState(i18next.t("user:Send Code"));
|
||||
const [sendCodeCoolDown, setCoolDown] = React.useState(false);
|
||||
const {buttonText, destType, coolDownTime} = props;
|
||||
|
||||
const showModal = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
let dest = document.getElementById("dest").value;
|
||||
let code = document.getElementById("code").value;
|
||||
if (dest === "") {
|
||||
Setting.showMessage("error", i18next.t("user:Empty " + destType));
|
||||
return;
|
||||
}
|
||||
if (code === "") {
|
||||
Setting.showMessage("error", i18next.t("user:Empty Code"));
|
||||
return;
|
||||
}
|
||||
setConfirmLoading(true);
|
||||
UserBackend.resetEmailOrPhone(dest, destType, code).then(res => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("user:" + destType + " reset"));
|
||||
window.location.reload();
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t("user:" + res.msg));
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const countDown = (second) => {
|
||||
if (second <= 0) {
|
||||
setSendButtonText(i18next.t("user:Send Code"));
|
||||
setCoolDown(false);
|
||||
return;
|
||||
}
|
||||
setSendButtonText(second);
|
||||
setTimeout(() => countDown(second - 1), 1000);
|
||||
}
|
||||
|
||||
const sendCode = () => {
|
||||
if (sendCodeCoolDown) return;
|
||||
let dest = document.getElementById("dest").value;
|
||||
if (dest === "") {
|
||||
Setting.showMessage("error", i18next.t("user:Empty " + destType));
|
||||
return;
|
||||
}
|
||||
UserBackend.sendCode(dest, destType).then(res => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("user:Code Sent"));
|
||||
setCoolDown(true);
|
||||
countDown(coolDownTime);
|
||||
} else {
|
||||
Setting.showMessage("error", i18next.t("user:" + res.msg));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let placeHolder = "";
|
||||
if (destType === "email") placeHolder = i18next.t("user:Input your email");
|
||||
else if (destType === "phone") placeHolder = i18next.t("user:Input your phone number");
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Button style={{marginTop: '22px'}} type="default" onClick={showModal}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<Modal
|
||||
title={buttonText}
|
||||
visible={visible}
|
||||
okText={buttonText}
|
||||
cancelText={i18next.t("user:Cancel")}
|
||||
confirmLoading={confirmLoading}
|
||||
onCancel={handleCancel}
|
||||
onOk={handleOk}
|
||||
width={600}
|
||||
>
|
||||
<Col style={{margin: "0px auto 40px auto", width: 1000, height: 300}}>
|
||||
<Row style={{width: "100%", marginBottom: "20px"}}>
|
||||
<Input addonBefore={i18next.t("user:New " + destType)} id="dest" placeholder={placeHolder}
|
||||
addonAfter={<button style={{width: "90px", border: "none", backgroundColor: "#fff"}} onClick={sendCode}>{" " + sendButtonText + " "}</button>}
|
||||
/>
|
||||
|
||||
</Row>
|
||||
<Row style={{width: "100%", marginBottom: "20px"}}>
|
||||
<Input addonBefore={i18next.t("user:Code You Received")} placeholder={i18next.t("user:Enter your code")} id="code"/>
|
||||
</Row>
|
||||
</Col>
|
||||
</Modal>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetModal;
|
@ -25,6 +25,7 @@ import * as ApplicationBackend from "./backend/ApplicationBackend";
|
||||
import * as ProviderBackend from "./backend/ProviderBackend";
|
||||
import * as Provider from "./auth/Provider";
|
||||
import PasswordModal from "./PasswordModal";
|
||||
import ResetModal from "./ResetModal";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@ -275,9 +276,8 @@ class UserEditPage extends React.Component {
|
||||
{i18next.t("general:Email")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.user.email} onChange={e => {
|
||||
this.updateUserField('email', e.target.value);
|
||||
}} />
|
||||
<Input value={this.state.user.email} disabled />
|
||||
{ this.state.user.id === this.props.account.id ? (<ResetModal buttonText={i18next.t("user:Reset Email")} destType={"email"} coolDownTime={60}/>) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
@ -285,14 +285,8 @@ class UserEditPage extends React.Component {
|
||||
{i18next.t("general:Phone")}:
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input addonBefore={
|
||||
<Select virtual={false} style={{width: 70}} value={this.state.user.phonePrefix} onChange={(value => {this.updateUserField('phonePrefix', value);})}>
|
||||
<Option value="1">+1</Option>
|
||||
<Option value="86">+86</Option>
|
||||
</Select>
|
||||
} value={this.state.user.phone} onChange={e => {
|
||||
this.updateUserField('phone', e.target.value);
|
||||
}} />
|
||||
<Input value={this.state.user.phone} disabled/>
|
||||
{ this.state.user.id === this.props.account.id ? (<ResetModal buttonText={i18next.t("user:Reset Phone")} destType={"phone"} coolDownTime={60}/>) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: '20px'}} >
|
||||
|
@ -52,7 +52,6 @@ class UserListPage extends React.Component {
|
||||
displayName: `New User - ${this.state.users.length}`,
|
||||
avatar: "https://casbin.org/img/casbin.svg",
|
||||
email: "user@example.com",
|
||||
phonePrefix: "86",
|
||||
phone: "12345678",
|
||||
affiliation: "Example Inc.",
|
||||
tag: "staff",
|
||||
|
@ -133,19 +133,6 @@ class SignupPage extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
const prefixSelector = (
|
||||
<Form.Item name="phonePrefix" noStyle>
|
||||
<Select
|
||||
style={{
|
||||
width: 80,
|
||||
}}
|
||||
>
|
||||
<Option value="1">+1</Option>
|
||||
<Option value="86">+86</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form
|
||||
{...formItemLayout}
|
||||
@ -156,7 +143,6 @@ class SignupPage extends React.Component {
|
||||
initialValues={{
|
||||
application: application.name,
|
||||
organization: application.organization,
|
||||
phonePrefix: '86',
|
||||
}}
|
||||
style={{width: !Setting.isMobile() ? "400px" : "250px"}}
|
||||
size="large"
|
||||
@ -285,7 +271,6 @@ class SignupPage extends React.Component {
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
addonBefore={prefixSelector}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
|
@ -92,3 +92,26 @@ export function setPassword(userOwner, userName, oldPassword, newPassword) {
|
||||
body: formData
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function sendCode(dest, type) {
|
||||
let formData = new FormData();
|
||||
formData.append("dest", dest);
|
||||
formData.append("type", type);
|
||||
return fetch(`${Setting.ServerUrl}/api/send-verification-code`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function resetEmailOrPhone(dest, type, code) {
|
||||
let formData = new FormData();
|
||||
formData.append("dest", dest);
|
||||
formData.append("type", type);
|
||||
formData.append("code", code);
|
||||
return fetch(`${Setting.ServerUrl}/api/reset-email-or-phone`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: formData
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
@ -29,7 +29,8 @@
|
||||
"Users under all organizations": "Users under all organizations",
|
||||
"OAuth providers": "OAuth providers",
|
||||
"Applications that requires authentication": "Applications that requires authentication",
|
||||
"Swagger": "Swagger"
|
||||
"Swagger": "Swagger",
|
||||
"Phone Prefix": "Phone Prefix"
|
||||
},
|
||||
"signup":
|
||||
{
|
||||
@ -115,7 +116,28 @@
|
||||
"Invalid new password": "Invalid new password",
|
||||
"Password": "Password",
|
||||
"Cancel": "Cancel",
|
||||
"input password": "input password"
|
||||
"input password": "input password",
|
||||
"Reset Email": "Reset Email",
|
||||
"Reset Phone": "Reset Phone",
|
||||
"Send Code": "Send Code",
|
||||
"Empty email": "Empty Email",
|
||||
"Empty phone": "Empty Phone",
|
||||
"Empty Code": "Empty Code",
|
||||
"phone reset": "Phone Reset",
|
||||
"email reset": "Email Reset",
|
||||
"Code Sent": "Code Sent",
|
||||
"Input your email": "Input your email",
|
||||
"Input your phone number": "Input your phone number",
|
||||
"New phone": "New Phone",
|
||||
"New email": "New Email",
|
||||
"Code You Received": "Code You Received",
|
||||
"Enter your code": "Enter your code",
|
||||
"You can only send one code in 60s.": "You can only send one code in 60s.",
|
||||
"Code has not been sent yet!": "Code has not been sent yet!",
|
||||
"You should verify your code in 5 min!": "You should verify your code in 5 min!",
|
||||
"Wrong code!": "Wrong code!",
|
||||
"Invalid phone number": "Invalid phone number",
|
||||
"Invalid Email address": "Invalid Email address"
|
||||
},
|
||||
"application":
|
||||
{
|
||||
|
@ -29,7 +29,8 @@
|
||||
"Users under all organizations": "所有组织里的用户",
|
||||
"OAuth providers": "OAuth提供方",
|
||||
"Applications that requires authentication": "需要鉴权的应用",
|
||||
"Swagger": "API总览"
|
||||
"Swagger": "API总览",
|
||||
"Phone Prefix": "电话前缀"
|
||||
},
|
||||
"signup":
|
||||
{
|
||||
@ -117,7 +118,28 @@
|
||||
"Invalid new password": "非法的新密码。",
|
||||
"Password": "密码",
|
||||
"Cancel": "取消",
|
||||
"input password": "输入密码"
|
||||
"input password": "输入密码",
|
||||
"Reset Email": "重设邮箱",
|
||||
"Reset Phone": "重设电话",
|
||||
"Send Code": "发送验证码",
|
||||
"Empty email": "邮箱为空",
|
||||
"Empty phone": "电话为空",
|
||||
"Empty Code": "验证码为空",
|
||||
"phone reset": "电话已设置",
|
||||
"email reset": "邮箱已设置",
|
||||
"Code Sent": "验证码已发送",
|
||||
"Input your email": "请输入邮箱",
|
||||
"Input your phone number": "输入电话号码",
|
||||
"New phone": "新的电话号",
|
||||
"New email": "新的邮箱",
|
||||
"Code You Received": "你收到的验证码",
|
||||
"Enter your code": "输入你的验证码",
|
||||
"You can only send one code in 60s.": "每分钟你只能发送一次验证码",
|
||||
"Code has not been sent yet!": "你还没有发送验证码",
|
||||
"You should verify your code in 5 min!": "验证码已超时。你应该在 5 分钟内完成验证。",
|
||||
"Wrong code!": "验证码错误!",
|
||||
"Invalid phone number": "手机号格式错误",
|
||||
"Invalid Email address": "邮箱格式错误"
|
||||
},
|
||||
"application":
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user