mirror of
https://gitee.com/goploy/goploy.git
synced 2024-11-30 03:07:59 +08:00
commit
76ccb466aa
@ -11,9 +11,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/wenlng/go-captcha/captcha"
|
||||
"github.com/zhenorzz/goploy/cmd/server/api"
|
||||
"github.com/zhenorzz/goploy/cmd/server/api/middleware"
|
||||
"github.com/zhenorzz/goploy/config"
|
||||
"github.com/zhenorzz/goploy/internal/cache"
|
||||
"github.com/zhenorzz/goploy/internal/media"
|
||||
"github.com/zhenorzz/goploy/internal/model"
|
||||
"github.com/zhenorzz/goploy/internal/server"
|
||||
@ -21,6 +23,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -40,6 +43,9 @@ func (u User) Handler() []server.Route {
|
||||
server.NewRoute("/user/remove", http.MethodDelete, u.Remove).Permissions(config.DeleteMember).LogFunc(middleware.AddOPLog),
|
||||
server.NewWhiteRoute("/user/mediaLogin", http.MethodPost, u.MediaLogin).LogFunc(middleware.AddLoginLog),
|
||||
server.NewWhiteRoute("/user/getMediaLoginUrl", http.MethodGet, u.GetMediaLoginUrl),
|
||||
server.NewWhiteRoute("/user/getCaptchaConfig", http.MethodGet, u.GetCaptchaConfig),
|
||||
server.NewWhiteRoute("/user/getCaptcha", http.MethodGet, u.GetCaptcha),
|
||||
server.NewWhiteRoute("/user/checkCaptcha", http.MethodPost, u.CheckCaptcha),
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,14 +58,30 @@ func (u User) Handler() []server.Route {
|
||||
// @Router /user/login [post]
|
||||
func (User) Login(gp *server.Goploy) server.Response {
|
||||
type ReqData struct {
|
||||
Account string `json:"account" validate:"required,min=1,max=25"`
|
||||
Password string `json:"password" validate:"required,password"`
|
||||
Account string `json:"account" validate:"required,min=1,max=25"`
|
||||
Password string `json:"password" validate:"required,password"`
|
||||
CaptchaKey string `json:"captchaKey" validate:"omitempty"`
|
||||
}
|
||||
var reqData ReqData
|
||||
if err := gp.Decode(&reqData); err != nil {
|
||||
return response.JSON{Code: response.IllegalParam, Message: err.Error()}
|
||||
}
|
||||
|
||||
userCache := cache.GetUserCache()
|
||||
|
||||
if config.Toml.Captcha.Enabled && userCache.IsShowCaptcha(reqData.Account) {
|
||||
captchaCache := cache.GetCaptchaCache()
|
||||
if !captchaCache.IsChecked(reqData.CaptchaKey) {
|
||||
return response.JSON{Code: response.Error, Message: "Captcha error, please check captcha again"}
|
||||
}
|
||||
// captcha should be deleted after check
|
||||
captchaCache.Delete(reqData.CaptchaKey)
|
||||
}
|
||||
|
||||
if userCache.IsLock(reqData.Account) {
|
||||
return response.JSON{Code: response.Error, Message: "Your account has been locked, please retry login in 15 minutes"}
|
||||
}
|
||||
|
||||
userData, err := model.User{Account: reqData.Account}.GetDataByAccount()
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return response.JSON{Code: response.Error, Message: "We couldn't verify your identity. Please confirm if your username and password are correct."}
|
||||
@ -108,10 +130,17 @@ func (User) Login(gp *server.Goploy) server.Response {
|
||||
}
|
||||
} else {
|
||||
if err := userData.Validate(reqData.Password); err != nil {
|
||||
errorTimes := userCache.IncErrorTimes(reqData.Account, cache.UserCacheExpireTime, cache.UserCacheShowCaptchaTime)
|
||||
// error times over 5 times, then lock the account 15 minutes
|
||||
if errorTimes >= cache.UserCacheMaxErrorTimes {
|
||||
userCache.LockAccount(reqData.Account, cache.UserCacheLockTime)
|
||||
}
|
||||
return response.JSON{Code: response.Deny, Message: err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
userCache.DeleteShowCaptcha(reqData.Account)
|
||||
|
||||
if userData.State == model.Disable {
|
||||
return response.JSON{Code: response.AccountDisabled, Message: "Account is disabled"}
|
||||
}
|
||||
@ -566,3 +595,97 @@ func (User) GetMediaLoginUrl(gp *server.Goploy) server.Response {
|
||||
}{Dingtalk: dingtalk, Feishu: feishu},
|
||||
}
|
||||
}
|
||||
|
||||
func (User) GetCaptchaConfig(gp *server.Goploy) server.Response {
|
||||
return response.JSON{
|
||||
Data: struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}{
|
||||
Enabled: config.Toml.Captcha.Enabled,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (User) GetCaptcha(gp *server.Goploy) server.Response {
|
||||
type ReqData struct {
|
||||
Language string `schema:"language" validate:"required"`
|
||||
}
|
||||
|
||||
var reqData ReqData
|
||||
if err := gp.Decode(&reqData); err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
capt := captcha.GetCaptcha()
|
||||
if reqData.Language == "zh-cn" {
|
||||
chars := captcha.GetCaptchaDefaultChars()
|
||||
_ = capt.SetRangChars(*chars)
|
||||
} else {
|
||||
chars := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
_ = capt.SetRangChars(strings.Split(chars, ""))
|
||||
}
|
||||
|
||||
dots, b64, tb64, key, err := capt.Generate()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.AccountDisabled, Message: "generate captcha fail, error msg:" + err.Error()}
|
||||
}
|
||||
|
||||
captchaCache := cache.GetCaptchaCache()
|
||||
captchaCache.Set(key, dots, 2*time.Minute)
|
||||
|
||||
return response.JSON{
|
||||
Data: struct {
|
||||
Base64 string `json:"base64"`
|
||||
ThumbBase64 string `json:"thumbBase64"`
|
||||
Key string `json:"key"`
|
||||
}{
|
||||
Base64: b64,
|
||||
ThumbBase64: tb64,
|
||||
Key: key,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (User) CheckCaptcha(gp *server.Goploy) server.Response {
|
||||
type ReqData struct {
|
||||
Key string `json:"key" validate:"required"`
|
||||
Dots []int64 `json:"dots" validate:"required"`
|
||||
RedirectUri string `json:"redirectUri" validate:"omitempty"`
|
||||
}
|
||||
|
||||
var reqData ReqData
|
||||
if err := gp.Decode(&reqData); err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
captchaCache := cache.GetCaptchaCache()
|
||||
dotsCache, ok := captchaCache.Get(reqData.Key)
|
||||
if !ok {
|
||||
return response.JSON{Code: response.Error, Message: "Illegal key, please refresh the captcha again"}
|
||||
}
|
||||
dots, _ := dotsCache.(map[int]captcha.CharDot)
|
||||
|
||||
check := false
|
||||
if (len(dots) * 2) == len(reqData.Dots) {
|
||||
for i, dot := range dots {
|
||||
j := i * 2
|
||||
k := i*2 + 1
|
||||
sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", reqData.Dots[j]), 64)
|
||||
sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", reqData.Dots[k]), 64)
|
||||
|
||||
check = captcha.CheckPointDistWithPadding(int64(sx), int64(sy), int64(dot.Dx), int64(dot.Dy), int64(dot.Width), int64(dot.Height), 15)
|
||||
if !check {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !check {
|
||||
return response.JSON{Code: response.Error, Message: "check captcha fail"}
|
||||
}
|
||||
|
||||
// set captcha key checked for login verify
|
||||
captchaCache.Set(reqData.Key, true, 2*time.Minute)
|
||||
|
||||
return response.JSON{}
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ type Config struct {
|
||||
Dingtalk DingtalkConfig `toml:"dingtalk"`
|
||||
Feishu FeishuConfig `toml:"feishu"`
|
||||
CORS CORSConfig `toml:"cors"`
|
||||
Captcha CaptchaConfig `toml:"captcha"`
|
||||
Cache CacheConfig `toml:"cache"`
|
||||
}
|
||||
|
||||
type APPConfig struct {
|
||||
@ -91,6 +93,14 @@ type FeishuConfig struct {
|
||||
AppSecret string `toml:"appSecret"`
|
||||
}
|
||||
|
||||
type CaptchaConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Type string `toml:"type"`
|
||||
}
|
||||
|
||||
var Toml Config
|
||||
|
||||
func InitToml() {
|
||||
|
3
go.mod
3
go.mod
@ -18,6 +18,7 @@ require (
|
||||
github.com/pkg/sftp v1.13.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/vearutop/statigz v1.1.8
|
||||
github.com/wenlng/go-captcha v1.2.5
|
||||
golang.org/x/crypto v0.1.0
|
||||
gopkg.in/go-playground/validator.v9 v9.31.0
|
||||
)
|
||||
@ -25,6 +26,7 @@ require (
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
@ -32,6 +34,7 @@ require (
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/image v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
)
|
||||
|
26
go.sum
26
go.sum
@ -21,6 +21,8 @@ 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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
@ -62,26 +64,50 @@ github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiU
|
||||
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vearutop/statigz v1.1.8 h1:IJgQHx6EomuYOYd2TzFt3haP+BIzV471zn7aepRiLHA=
|
||||
github.com/vearutop/statigz v1.1.8/go.mod h1:pfzrpvgLRnFeSVZd9iUYrpYDLqbV+RgeCfizr3ZFf44=
|
||||
github.com/wenlng/go-captcha v1.2.5 h1:zA0/fovEl9oAhSg+KwHBwmq99GeeAXknWx6wYKjhjTg=
|
||||
github.com/wenlng/go-captcha v1.2.5/go.mod h1:QgPgpEURSa37gF3GtojNoNRwbMwuatSBx5NXrzASOb0=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
|
||||
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
|
@ -47,4 +47,10 @@ appSecret = ''
|
||||
|
||||
[feishu]
|
||||
appKey = ''
|
||||
appSecret = ''
|
||||
appSecret = ''
|
||||
|
||||
[captcha]
|
||||
enabled = false
|
||||
|
||||
[cache]
|
||||
type = 'memory'
|
10
internal/cache/captcha.go
vendored
Normal file
10
internal/cache/captcha.go
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
package cache
|
||||
|
||||
import "time"
|
||||
|
||||
type Captcha interface {
|
||||
Get(key string) (interface{}, bool)
|
||||
Set(key string, value interface{}, ttl time.Duration)
|
||||
Delete(key string)
|
||||
IsChecked(key string) bool
|
||||
}
|
8
internal/cache/dingtalk.go
vendored
Normal file
8
internal/cache/dingtalk.go
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
package cache
|
||||
|
||||
import "time"
|
||||
|
||||
type DingtalkAccessToken interface {
|
||||
Get(key string) (string, bool)
|
||||
Set(key string, value string, ttl time.Duration)
|
||||
}
|
37
internal/cache/factory.go
vendored
Normal file
37
internal/cache/factory.go
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/zhenorzz/goploy/config"
|
||||
"github.com/zhenorzz/goploy/internal/cache/memory"
|
||||
)
|
||||
|
||||
const MemoryCache = "memory"
|
||||
|
||||
var cacheType = config.Toml.Cache.Type
|
||||
|
||||
func GetUserCache() User {
|
||||
switch cacheType {
|
||||
case MemoryCache:
|
||||
return memory.GetUserCache()
|
||||
default:
|
||||
return memory.GetUserCache()
|
||||
}
|
||||
}
|
||||
|
||||
func GetCaptchaCache() Captcha {
|
||||
switch cacheType {
|
||||
case MemoryCache:
|
||||
return memory.GetCaptchaCache()
|
||||
default:
|
||||
return memory.GetCaptchaCache()
|
||||
}
|
||||
}
|
||||
|
||||
func GetDingTalkAccessTokenCache() DingtalkAccessToken {
|
||||
switch cacheType {
|
||||
case MemoryCache:
|
||||
return memory.GetDingTalkAccessTokenCache()
|
||||
default:
|
||||
return memory.GetDingTalkAccessTokenCache()
|
||||
}
|
||||
}
|
80
internal/cache/memory/captcha.go
vendored
Normal file
80
internal/cache/memory/captcha.go
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CaptchaCache struct {
|
||||
data map[string]captcha
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type captcha struct {
|
||||
dots interface{}
|
||||
expireIn time.Time
|
||||
}
|
||||
|
||||
var captchaCache = &CaptchaCache{
|
||||
data: make(map[string]captcha),
|
||||
}
|
||||
|
||||
func (c *CaptchaCache) Get(key string) (interface{}, bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
v, ok := c.data[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if !v.expireIn.IsZero() && v.expireIn.After(time.Now()) {
|
||||
return v.dots, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *CaptchaCache) Set(key string, value interface{}, ttl time.Duration) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
var expireIn time.Time
|
||||
|
||||
if ttl > 0 {
|
||||
expireIn = time.Now().Add(ttl)
|
||||
}
|
||||
|
||||
c.data[key] = captcha{
|
||||
dots: value,
|
||||
expireIn: expireIn,
|
||||
}
|
||||
|
||||
time.AfterFunc(ttl, func() {
|
||||
delete(c.data, key)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CaptchaCache) Delete(key string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
delete(c.data, key)
|
||||
}
|
||||
|
||||
func (c *CaptchaCache) IsChecked(key string) bool {
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if check, ok := c.Get(key); ok {
|
||||
_check, _ := check.(bool)
|
||||
return _check
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func GetCaptchaCache() *CaptchaCache {
|
||||
return captchaCache
|
||||
}
|
60
internal/cache/memory/dingtalk.go
vendored
Normal file
60
internal/cache/memory/dingtalk.go
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AccessTokenCache struct {
|
||||
data map[string]accessToken
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type accessToken struct {
|
||||
accessToken string
|
||||
expireIn time.Time
|
||||
}
|
||||
|
||||
var accessTokenCache = &AccessTokenCache{
|
||||
data: make(map[string]accessToken),
|
||||
}
|
||||
|
||||
func (ac *AccessTokenCache) Get(key string) (string, bool) {
|
||||
ac.mutex.RLock()
|
||||
defer ac.mutex.RUnlock()
|
||||
|
||||
v, ok := ac.data[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if !v.expireIn.IsZero() && v.expireIn.After(time.Now()) {
|
||||
return v.accessToken, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (ac *AccessTokenCache) Set(key string, value string, ttl time.Duration) {
|
||||
ac.mutex.Lock()
|
||||
defer ac.mutex.Unlock()
|
||||
|
||||
var expireIn time.Time
|
||||
|
||||
if ttl > 0 {
|
||||
expireIn = time.Now().Add(ttl)
|
||||
}
|
||||
|
||||
ac.data[key] = accessToken{
|
||||
accessToken: value,
|
||||
expireIn: expireIn,
|
||||
}
|
||||
|
||||
time.AfterFunc(ttl, func() {
|
||||
delete(ac.data, key)
|
||||
})
|
||||
}
|
||||
|
||||
func GetDingTalkAccessTokenCache() *AccessTokenCache {
|
||||
return accessTokenCache
|
||||
}
|
127
internal/cache/memory/user.go
vendored
Normal file
127
internal/cache/memory/user.go
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UserCacheKey = "login_error_times_"
|
||||
UserCacheLockKey = "login_lock_"
|
||||
UserCacheShowCaptchaKey = "login_show_captcha_"
|
||||
)
|
||||
|
||||
type UserCache struct {
|
||||
data map[string]user
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type user struct {
|
||||
times int
|
||||
expireIn time.Time
|
||||
}
|
||||
|
||||
var userCache = &UserCache{
|
||||
data: make(map[string]user),
|
||||
}
|
||||
|
||||
func (uc *UserCache) IncErrorTimes(account string, expireTime time.Duration, showCaptchaTime time.Duration) int {
|
||||
uc.Lock()
|
||||
defer uc.Unlock()
|
||||
|
||||
cacheKey := getCacheKey(account)
|
||||
|
||||
times := 0
|
||||
v, ok := uc.data[cacheKey]
|
||||
if ok && !v.expireIn.IsZero() && v.expireIn.After(time.Now()) {
|
||||
times = v.times
|
||||
}
|
||||
|
||||
times += 1
|
||||
|
||||
uc.data[cacheKey] = user{
|
||||
times: times,
|
||||
expireIn: time.Now().Add(expireTime),
|
||||
}
|
||||
time.AfterFunc(expireTime, func() {
|
||||
delete(uc.data, cacheKey)
|
||||
})
|
||||
|
||||
// show captcha
|
||||
showCaptchaKey := getShowCaptchaKey(account)
|
||||
uc.data[showCaptchaKey] = user{
|
||||
times: 1,
|
||||
expireIn: time.Now().Add(showCaptchaTime),
|
||||
}
|
||||
time.AfterFunc(showCaptchaTime, func() {
|
||||
delete(uc.data, showCaptchaKey)
|
||||
})
|
||||
|
||||
return times
|
||||
}
|
||||
|
||||
func (uc *UserCache) LockAccount(account string, lockTime time.Duration) {
|
||||
uc.Lock()
|
||||
defer uc.Unlock()
|
||||
|
||||
lockKey := getLockKey(account)
|
||||
|
||||
uc.data[lockKey] = user{
|
||||
times: 1,
|
||||
expireIn: time.Now().Add(lockTime),
|
||||
}
|
||||
|
||||
time.AfterFunc(lockTime, func() {
|
||||
delete(uc.data, lockKey)
|
||||
})
|
||||
|
||||
cacheKey := getCacheKey(account)
|
||||
|
||||
_, ok := uc.data[cacheKey]
|
||||
if ok {
|
||||
delete(uc.data, cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *UserCache) IsLock(account string) bool {
|
||||
uc.RLock()
|
||||
defer uc.RUnlock()
|
||||
|
||||
lockKey := getLockKey(account)
|
||||
v, ok := uc.data[lockKey]
|
||||
|
||||
return ok && !v.expireIn.IsZero() && v.expireIn.After(time.Now()) && v.times > 0
|
||||
}
|
||||
|
||||
func (uc *UserCache) IsShowCaptcha(account string) bool {
|
||||
uc.RLock()
|
||||
defer uc.RUnlock()
|
||||
|
||||
showCaptchaKey := getShowCaptchaKey(account)
|
||||
v, ok := uc.data[showCaptchaKey]
|
||||
|
||||
return ok && !v.expireIn.IsZero() && v.expireIn.After(time.Now()) && v.times > 0
|
||||
}
|
||||
|
||||
func (uc *UserCache) DeleteShowCaptcha(account string) {
|
||||
uc.Lock()
|
||||
defer uc.Unlock()
|
||||
|
||||
delete(uc.data, getShowCaptchaKey(account))
|
||||
}
|
||||
|
||||
func getCacheKey(account string) string {
|
||||
return UserCacheKey + account
|
||||
}
|
||||
|
||||
func getLockKey(account string) string {
|
||||
return UserCacheLockKey + account
|
||||
}
|
||||
|
||||
func getShowCaptchaKey(account string) string {
|
||||
return UserCacheShowCaptchaKey + account
|
||||
}
|
||||
|
||||
func GetUserCache() *UserCache {
|
||||
return userCache
|
||||
}
|
18
internal/cache/user.go
vendored
Normal file
18
internal/cache/user.go
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
package cache
|
||||
|
||||
import "time"
|
||||
|
||||
type User interface {
|
||||
IncErrorTimes(account string, expireTime time.Duration, showCaptchaTime time.Duration) int
|
||||
LockAccount(account string, lockTime time.Duration)
|
||||
IsLock(account string) bool
|
||||
IsShowCaptcha(account string) bool
|
||||
DeleteShowCaptcha(account string)
|
||||
}
|
||||
|
||||
const (
|
||||
UserCacheMaxErrorTimes = 5
|
||||
UserCacheExpireTime = 5 * time.Minute
|
||||
UserCacheLockTime = 15 * time.Minute
|
||||
UserCacheShowCaptchaTime = 15 * time.Minute
|
||||
)
|
@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/zhenorzz/goploy/config"
|
||||
"github.com/zhenorzz/goploy/internal/cache"
|
||||
"github.com/zhenorzz/goploy/internal/media/dingtalk/api"
|
||||
"github.com/zhenorzz/goploy/internal/media/dingtalk/api/access_token"
|
||||
"github.com/zhenorzz/goploy/internal/media/dingtalk/api/contact"
|
||||
@ -21,7 +22,7 @@ type Dingtalk struct {
|
||||
Key string
|
||||
Secret string
|
||||
Client *http.Client
|
||||
Cache *AccessTokenCache
|
||||
Cache cache.DingtalkAccessToken
|
||||
Method string
|
||||
Api string
|
||||
Query url.Values
|
||||
@ -33,7 +34,7 @@ func (d *Dingtalk) Login(authCode string, _ string) (string, error) {
|
||||
d.Key = config.Toml.Dingtalk.AppKey
|
||||
d.Secret = config.Toml.Dingtalk.AppSecret
|
||||
d.Client = &http.Client{}
|
||||
d.Cache = GetCache()
|
||||
d.Cache = cache.GetDingTalkAccessTokenCache()
|
||||
|
||||
userAccessTokenInfo, err := d.GetUserAccessToken(authCode)
|
||||
if err != nil {
|
||||
|
@ -19,6 +19,7 @@ export class Login extends Request {
|
||||
public param: {
|
||||
account: string
|
||||
password: string
|
||||
captchaKey: string
|
||||
}
|
||||
public declare datagram: {
|
||||
namespaceList: { id: number; name: string; roleId: number }[]
|
||||
@ -172,3 +173,41 @@ export class UserChangePassword extends Request {
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
||||
export class GetCaptchaConfig extends Request {
|
||||
readonly url = '/user/getCaptchaConfig'
|
||||
readonly method = 'get'
|
||||
public declare datagram: {
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class GetCaptcha extends Request {
|
||||
readonly url = '/user/getCaptcha'
|
||||
readonly method = 'get'
|
||||
public param: {
|
||||
language: any
|
||||
}
|
||||
public declare datagram: {
|
||||
base64: string
|
||||
thumbBase64: string
|
||||
key: string
|
||||
}
|
||||
constructor(param: GetCaptcha['param']) {
|
||||
super()
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
||||
export class CheckCaptcha extends Request {
|
||||
readonly url = '/user/checkCaptcha'
|
||||
readonly method = 'post'
|
||||
public param: {
|
||||
dots: number[]
|
||||
key: string
|
||||
}
|
||||
constructor(param: CheckCaptcha['param']) {
|
||||
super()
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
@ -404,5 +404,15 @@
|
||||
],
|
||||
"removeProjectTaskTips": "This action will delete the crontab task in {projectName}, continue?",
|
||||
"publishCommitTips": "This action will rebuild {commit}, continue?"
|
||||
},
|
||||
"loginPage": {
|
||||
"captchaDefault": "Click on the button for check captcha",
|
||||
"captchaCheck": "Checking captcha...",
|
||||
"captchaError": "Check captcha fail",
|
||||
"captchaOver": "Too many clicks<em>retry</em>",
|
||||
"captchaSuccess": "Captcha passed",
|
||||
"captchaRetry": "Click to retry",
|
||||
"captchaTips": "Please click",
|
||||
"captchaInOrder": "in order"
|
||||
}
|
||||
}
|
||||
|
@ -374,5 +374,15 @@
|
||||
"reviewStateOption": ["待审", "已审", "拒审"],
|
||||
"removeProjectTaskTips": "此操作删除{projectName}的定时任务, 是否继续?",
|
||||
"publishCommitTips": "此操作将重新构建{commit}, 是否继续?"
|
||||
},
|
||||
"loginPage": {
|
||||
"captchaDefault": "点击按键进行人机验证",
|
||||
"captchaCheck": "正在进行人机验证...",
|
||||
"captchaError": "人机验证失败",
|
||||
"captchaOver": "点击次数过多",
|
||||
"captchaSuccess": "人机验证已通过",
|
||||
"captchaRetry": "点击重试",
|
||||
"captchaTips": "请在下图",
|
||||
"captchaInOrder": "依次点击"
|
||||
}
|
||||
}
|
||||
|
@ -31,9 +31,13 @@ const mutations: MutationTree<UserState> = {
|
||||
const actions: ActionTree<UserState, RootState> = {
|
||||
// user login
|
||||
login(_, userInfo) {
|
||||
const { account, password } = userInfo
|
||||
const { account, password, captchaKey } = userInfo
|
||||
return new Promise((resolve, reject) => {
|
||||
new Login({ account: account.trim(), password: password })
|
||||
new Login({
|
||||
account: account.trim(),
|
||||
password: password,
|
||||
captchaKey: captchaKey,
|
||||
})
|
||||
.request()
|
||||
.then((response) => {
|
||||
const { data } = response
|
||||
|
389
web/src/views/login/components/GoCaptcha.vue
Normal file
389
web/src/views/login/components/GoCaptcha.vue
Normal file
@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="wg-cap-wrap">
|
||||
<div class="wg-cap-wrap__header">
|
||||
<span
|
||||
>{{ $t('loginPage.captchaTips')
|
||||
}}<em>{{ $t('loginPage.captchaInOrder') }}</em></span
|
||||
>
|
||||
<img
|
||||
v-if="thumbBase64Code"
|
||||
class="wg-cap-wrap__thumb"
|
||||
:src="thumbBase64Code"
|
||||
alt=" "
|
||||
/>
|
||||
</div>
|
||||
<div class="wg-cap-wrap__body" :style="style">
|
||||
<img
|
||||
v-if="imageBase64Code"
|
||||
class="wg-cap-wrap__picture"
|
||||
:src="imageBase64Code"
|
||||
alt=" "
|
||||
@click="handleClickPos($event)"
|
||||
/>
|
||||
<img
|
||||
class="wg-cap-wrap__loading"
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+"
|
||||
alt="正在加载中..."
|
||||
/>
|
||||
<div v-for="(dot, key) in dots" :key="key">
|
||||
<div
|
||||
class="wg-cap-wrap__dot"
|
||||
:style="`top: ${dot.y}px; left:${dot.x}px;`"
|
||||
>
|
||||
<span>{{ dot.index }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wg-cap-wrap__footer">
|
||||
<div class="wg-cap-wrap__ico">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg=="
|
||||
alt="关闭"
|
||||
@click="handleCloseEvent"
|
||||
/>
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4="
|
||||
alt="刷新"
|
||||
@click="handleRefreshEvent"
|
||||
/>
|
||||
</div>
|
||||
<div class="wg-cap-wrap__btn">
|
||||
<button @click="handleConfirmEvent">{{ $t('confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue'
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
width: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
calcPosType: {
|
||||
type: String,
|
||||
default: 'dom',
|
||||
validator: (value: string) => ['dom', 'screen'].includes(value),
|
||||
},
|
||||
maxDot: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
imageBase64: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
thumbBase64: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const dots = ref<{ x: number; y: number; index: number }[]>([])
|
||||
const imageBase64Code = ref('')
|
||||
const thumbBase64Code = ref('')
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue
|
||||
},
|
||||
() => {
|
||||
dots.value = []
|
||||
imageBase64Code.value = ''
|
||||
thumbBase64Code.value = ''
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => {
|
||||
return props.imageBase64
|
||||
},
|
||||
(val: string) => {
|
||||
dots.value = []
|
||||
imageBase64Code.value = val
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => {
|
||||
return props.thumbBase64
|
||||
},
|
||||
(val: string) => {
|
||||
dots.value = []
|
||||
thumbBase64Code.value = val
|
||||
}
|
||||
)
|
||||
const style = computed(() => {
|
||||
return `width:${props.width}; height:${props.height};`
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'refresh', 'confirm'])
|
||||
/**
|
||||
* @Description: 处理关闭事件
|
||||
*/
|
||||
function handleCloseEvent() {
|
||||
emit('close')
|
||||
dots.value = []
|
||||
imageBase64Code.value = ''
|
||||
thumbBase64Code.value = ''
|
||||
}
|
||||
/**
|
||||
* @Description: 处理刷新事件
|
||||
*/
|
||||
function handleRefreshEvent() {
|
||||
dots.value = []
|
||||
emit('refresh')
|
||||
}
|
||||
/**
|
||||
* @Description: 处理确认事件
|
||||
*/
|
||||
function handleConfirmEvent() {
|
||||
emit('confirm', dots.value)
|
||||
}
|
||||
/**
|
||||
* @Description: 处理dot
|
||||
* @param ev
|
||||
*/
|
||||
function handleClickPos(ev: any) {
|
||||
if (dots.value.length >= props.maxDot) {
|
||||
return
|
||||
}
|
||||
const e = ev || window.event
|
||||
e.preventDefault()
|
||||
const dom = e.currentTarget
|
||||
|
||||
const { domX, domY } = getDomXY(dom)
|
||||
// ===============================================
|
||||
// @notice 如 getDomXY 不准确可尝试使用 calcLocationLeft 或 calcLocationTop
|
||||
// const domX = calcLocationLeft(dom)
|
||||
// const domY = calcLocationTop(dom)
|
||||
// ===============================================
|
||||
|
||||
let mouseX =
|
||||
navigator.appName === 'Netscape' ? e.pageX : e.x + document.body.offsetTop
|
||||
let mouseY =
|
||||
navigator.appName === 'Netscape' ? e.pageY : e.y + document.body.offsetTop
|
||||
|
||||
if (props.calcPosType === 'screen') {
|
||||
mouseX = navigator.appName === 'Netscape' ? e.clientX : e.x
|
||||
mouseY = navigator.appName === 'Netscape' ? e.clientY : e.y
|
||||
}
|
||||
|
||||
// 计算点击的相对位置
|
||||
const xPos = mouseX - domX
|
||||
const yPos = mouseY - domY
|
||||
|
||||
// 转整形
|
||||
const xp = parseInt(xPos.toString())
|
||||
const yp = parseInt(yPos.toString())
|
||||
|
||||
// 减去点的一半
|
||||
dots.value.push({
|
||||
x: xp - 11,
|
||||
y: yp - 11,
|
||||
index: dots.value.length + 1,
|
||||
})
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @Description: 找到元素的屏幕位置
|
||||
* @param dom
|
||||
*/
|
||||
function getDomXY(dom: any) {
|
||||
let x = 0
|
||||
let y = 0
|
||||
if (dom.getBoundingClientRect) {
|
||||
let box = dom.getBoundingClientRect()
|
||||
let D = document.documentElement
|
||||
x =
|
||||
box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft
|
||||
y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop
|
||||
} else {
|
||||
while (dom !== document.body) {
|
||||
x += dom.offsetLeft
|
||||
y += dom.offsetTop
|
||||
dom = dom.offsetParent
|
||||
}
|
||||
}
|
||||
return {
|
||||
domX: x,
|
||||
domY: y,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wg-cap-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.wg-cap-wrap__header {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
.wg-cap-wrap__header span {
|
||||
padding-right: 5px;
|
||||
}
|
||||
.wg-cap-wrap__header span em {
|
||||
padding: 0 3px;
|
||||
font-weight: bold;
|
||||
color: #3e7cff;
|
||||
font-style: normal;
|
||||
}
|
||||
.wg-cap-wrap__header .wg-cap-wrap__image {
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.wg-cap-wrap__header .wg-cap-wrap__thumb {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
max-height: 100%;
|
||||
}
|
||||
.wg-cap-wrap__header .wg-cap-wrap__thumb.wg-cap-wrap__hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__body {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
background: #34383e;
|
||||
margin: auto;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wg-cap-wrap__body .wg-cap-wrap__picture {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
|
||||
/*height: 100%;*/
|
||||
/*max-width: 100%;*/
|
||||
/*max-height: 100%;*/
|
||||
/*object-fit: cover;*/
|
||||
/*text-align: center;*/
|
||||
}
|
||||
.wg-cap-wrap__body .wg-cap-wrap__picture.wg-cap-wrap__hidden {
|
||||
display: none;
|
||||
}
|
||||
.wg-cap-wrap__body .wg-cap-wrap__loading {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
margin-left: -34px;
|
||||
margin-top: -34px;
|
||||
line-height: 68px;
|
||||
text-align: center;
|
||||
}
|
||||
.wg-cap-wrap__body .wg-cap-wrap__dot {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #cedffe;
|
||||
background: #3e7cff;
|
||||
border: 2px solid #f7f9fb;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
-webkit-border-radius: 22px;
|
||||
-moz-border-radius: 22px;
|
||||
border-radius: 22px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wg-cap-wrap__footer {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
color: #34383e;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding-top: 15px;
|
||||
}
|
||||
.wg-cap-wrap__footer .wg-cap-wrap__ico {
|
||||
flex: 1;
|
||||
}
|
||||
.wg-cap-wrap__footer .wg-cap-wrap__ico img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #34383e;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.wg-cap-wrap__footer .wg-cap-wrap__btn {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
}
|
||||
.wg-cap-wrap__footer .wg-cap-wrap__btn button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
letter-spacing: 2px;
|
||||
text-align: center;
|
||||
padding: 9px 15px;
|
||||
font-size: 15px;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background-color: #409eff;
|
||||
border: 1px solid #409eff;
|
||||
-webkit-appearance: none;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.1s;
|
||||
font-weight: 500;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.wg-cap-wrap__footer .wg-cap-wrap__btn button:hover {
|
||||
background: #66b1ff;
|
||||
border-color: #66b1ff;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
392
web/src/views/login/components/GoCaptchaBtn.vue
Normal file
392
web/src/views/login/components/GoCaptchaBtn.vue
Normal file
@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<div class="wg-cap-btn" :style="style">
|
||||
<div class="wg-cap-btn__inner" :class="activeClass">
|
||||
<!-- wg-cap-active__default wg-cap-active__error wg-cap-active__over wg-cap-active__success -->
|
||||
<el-popover
|
||||
ref="popoverRef"
|
||||
placement="top"
|
||||
trigger="click"
|
||||
:width="326"
|
||||
@before-leave="handleCloseEvent"
|
||||
>
|
||||
<go-captcha
|
||||
v-model="popoverVisible"
|
||||
width="300px"
|
||||
height="240px"
|
||||
:max-dot="maxDot"
|
||||
:image-base64="imageBase64"
|
||||
:thumb-base64="thumbBase64"
|
||||
@close="handleCloseEvent"
|
||||
@refresh="handleRefreshEvent"
|
||||
@confirm="handleConfirmEvent"
|
||||
/>
|
||||
<template #reference>
|
||||
<div
|
||||
v-if="captchaStatus === 'default'"
|
||||
class="wg-cap-state__default"
|
||||
@click="handleBtnEvent"
|
||||
>
|
||||
<!-- init -->
|
||||
<div class="wg-cap-state__inner">
|
||||
<div class="wg-cap-btn__ico wg-cap-btn__verify">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSLlm77lsYJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiCgkgdmlld0JveD0iMCAwIDIwMCAyMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDIwMCAyMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojM0U3Q0ZGO30KCS5zdDF7ZmlsbDojRkZGRkZGO30KPC9zdHlsZT4KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMTAwIiBjeT0iMTAwIiByPSI5Ni4zIi8+CjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xNDAuOCw2NC40bC0zOS42LTExLjloLTIuNEw1OS4yLDY0LjRjLTEuNiwwLjgtMi44LDIuNC0yLjgsNHYyNC4xYzAsMjUuMywxNS44LDQ1LjksNDIuMyw1NC42CgljMC40LDAsMC44LDAuNCwxLjIsMC40YzAuNCwwLDAuOCwwLDEuMi0wLjRjMjYuNS04LjcsNDIuMy0yOC45LDQyLjMtNTQuNlY2OC4zQzE0My41LDY2LjgsMTQyLjMsNjUuMiwxNDAuOCw2NC40eiIvPgo8L3N2Zz4K"
|
||||
/>
|
||||
</div>
|
||||
<span class="wg-cap-btn__text">{{
|
||||
$t('loginPage.captchaDefault')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="captchaStatus === 'check'" class="wg-cap-state__check">
|
||||
<!-- check -->
|
||||
<div class="wg-cap-state__inner">
|
||||
<div class="wg-cap-btn__ico">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI3MDU1NTg2NTk0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEyMTEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTIwLjI1OTQ1NiA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjEyIiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PHBhdGggZD0iTTUxMS45OTk0ODggNTEyLjAwMTAyM20tMTE3LjkyMTcxMyAwYTExNS4yMzYgMTE1LjIzNiAwIDEgMCAyMzUuODQzNDI2IDAgMTE1LjIzNiAxMTUuMjM2IDAgMSAwLTIzNS44NDM0MjYgMFoiIHAtaWQ9IjEyMTMiIGZpbGw9IiNmZmEwMDAiPjwvcGF0aD48cGF0aCBkPSJNOTAzLjczOTUyMSA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjE0IiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PC9zdmc+"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<span class="wg-cap-btn__text">{{
|
||||
$t('loginPage.captchaCheck')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="captchaStatus === 'error'"
|
||||
class="wg-cap-state__error"
|
||||
@click="handleBtnEvent"
|
||||
>
|
||||
<!-- error -->
|
||||
<div class="wg-cap-state__inner">
|
||||
<div class="wg-cap-btn__ico">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
|
||||
alt="失败"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
>{{ $t('loginPage.captchaError') }}
|
||||
<em>{{ $t('loginPage.captchaRetry') }}</em></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="captchaStatus === 'over'"
|
||||
class="wg-cap-state__over"
|
||||
@click="handleBtnEvent"
|
||||
>
|
||||
<!-- too many times error -->
|
||||
<div class="wg-cap-state__inner">
|
||||
<div class="wg-cap-btn__ico">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
|
||||
alt="失败"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
>{{ $t('loginPage.captchaOver') }}
|
||||
<em>{{ $t('loginPage.captchaRetry') }}</em></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="captchaStatus === 'success'" class="wg-cap-state__success">
|
||||
<!-- success -->
|
||||
<div class="wg-cap-state__inner">
|
||||
<div class="wg-cap-btn__ico">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6IzVFQUEyRjt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODMuMywyNy4yTDEwMi40LDIuOWgtNC45TDE2LjcsMjcuMkMxMy40LDI4LjgsMTEsMzIsMTEsMzUuM3Y0OS40YzAsNTEuOCwzMi40LDkzLjksODYuNiwxMTEuNwoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOGMwLjgsMCwxLjYsMCwyLjQtMC44YzU0LjItMTcuOCw4Ni42LTU5LjEsODYuNi0xMTEuN1YzNS4zQzE4OSwzMiwxODYuNiwyOC44LDE4My4zLDI3LjJ6IE0xNDYuMSw4MS40CglsLTQ4LjUsNDguNWMtMS42LDEuNi0zLjIsMi40LTUuNywyLjRjLTIuNCwwLTQtMC44LTUuNy0yLjRMNjIsMTA1LjdjLTMuMi0zLjItMy4yLTguMSwwLTExLjNjMy4yLTMuMiw4LjEtMy4yLDExLjMsMGwxOC42LDE4LjYKCWw0Mi45LTQyLjljMy4yLTMuMiw4LjEtMy4yLDExLjMsMEMxNDkuNCw3My4zLDE0OS40LDc4LjIsMTQ2LjEsODEuNEwxNDYuMSw4MS40eiIvPgo8L3N2Zz4K"
|
||||
alt="成功"
|
||||
/>
|
||||
</div>
|
||||
<span>{{ $t('loginPage.captchaSuccess') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, ref } from 'vue'
|
||||
import GoCaptcha from './GoCaptcha.vue'
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (modelValue: string) =>
|
||||
['default', 'check', 'error', 'over', 'success'].indexOf(modelValue) > -1,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
maxDot: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
imageBase64: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
thumbBase64: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const popoverRef = ref()
|
||||
const popoverVisible = ref(false)
|
||||
const captchaStatus = ref('default')
|
||||
|
||||
const style = computed(() => {
|
||||
return `width:${props.width}; height:${props.height};`
|
||||
})
|
||||
|
||||
const activeClass = computed(() => {
|
||||
return `wg-cap-active__${captchaStatus.value}`
|
||||
})
|
||||
watch(popoverVisible, (val: boolean) => {
|
||||
if (val) {
|
||||
captchaStatus.value = 'check'
|
||||
emit('refresh')
|
||||
} else if (captchaStatus.value === 'check') {
|
||||
captchaStatus.value = props.modelValue
|
||||
}
|
||||
if (!val) {
|
||||
popoverRef.value.hide()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: (typeof props)['modelValue']) => {
|
||||
if (captchaStatus.value !== 'check') {
|
||||
captchaStatus.value = val
|
||||
}
|
||||
if (val === 'over' || val === 'success') {
|
||||
setTimeout(() => {
|
||||
popoverVisible.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(captchaStatus, (val: string) => {
|
||||
if (val !== 'check' && props.modelValue !== val) {
|
||||
emit('input', val)
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['input', 'refresh', 'confirm'])
|
||||
|
||||
function handleBtnEvent() {
|
||||
if (!['check', 'success'].includes(captchaStatus.value)) {
|
||||
setTimeout(() => {
|
||||
popoverVisible.value = true
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
function handleRefreshEvent() {
|
||||
captchaStatus.value = 'check'
|
||||
emit('refresh')
|
||||
}
|
||||
function handleConfirmEvent(data: any) {
|
||||
emit('confirm', data)
|
||||
}
|
||||
function handleCloseEvent() {
|
||||
popoverVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.wg-cap-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-btn__inner {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__default,
|
||||
.wg-cap-btn .wg-cap-state__check,
|
||||
.wg-cap-btn .wg-cap-state__error,
|
||||
.wg-cap-btn .wg-cap-state__success,
|
||||
.wg-cap-btn .wg-cap-state__over {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 13px;
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.1s;
|
||||
font-weight: 500;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
|
||||
visibility: hidden;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__default {
|
||||
color: #3e7cff;
|
||||
border: 1px solid #50a1ff;
|
||||
background: #ecf5ff;
|
||||
box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
|
||||
-webkit-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
|
||||
-moz-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__check {
|
||||
cursor: default;
|
||||
color: #ffa000;
|
||||
background: #fdf6ec;
|
||||
border: 1px solid #ffbe09;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__error {
|
||||
color: #ed4630;
|
||||
background: #fef0f0;
|
||||
border: 1px solid #ff5a34;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__over {
|
||||
color: #ed4630;
|
||||
background: #fef0f0;
|
||||
border: 1px solid #ff5a34;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__success {
|
||||
color: #5eaa2f;
|
||||
background: #f0f9eb;
|
||||
border: 1px solid #8bc640;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-active__default .wg-cap-state__default,
|
||||
.wg-cap-btn .wg-cap-active__error .wg-cap-state__error,
|
||||
.wg-cap-btn .wg-cap-active__over .wg-cap-state__over,
|
||||
.wg-cap-btn .wg-cap-active__success .wg-cap-state__success,
|
||||
.wg-cap-btn .wg-cap-active__check .wg-cap-state__check {
|
||||
visibility: visible;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__inner {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-state__inner em {
|
||||
padding-left: 5px;
|
||||
color: #3e7cff;
|
||||
font-style: normal;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
flex: 0;
|
||||
}
|
||||
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
float: left;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
5% {
|
||||
opacity: 0.05;
|
||||
}
|
||||
20% {
|
||||
opacity: 0.35;
|
||||
}
|
||||
65% {
|
||||
opacity: 0.01;
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(2) scaleY(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes ripple {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
5% {
|
||||
opacity: 0.05;
|
||||
}
|
||||
20% {
|
||||
opacity: 0.35;
|
||||
}
|
||||
65% {
|
||||
opacity: 0.01;
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(2) scaleY(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__verify::after {
|
||||
background: #409eff;
|
||||
-webkit-border-radius: 50px;
|
||||
-moz-border-radius: 50px;
|
||||
border-radius: 50px;
|
||||
content: '';
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
|
||||
animation: ripple 1.3s infinite;
|
||||
-moz-animation: ripple 1.3s infinite;
|
||||
-webkit-animation: ripple 1.3s infinite;
|
||||
animation-delay: 2s;
|
||||
-moz-animation-delay: 2s;
|
||||
-webkit-animation-delay: 2s;
|
||||
}
|
||||
|
||||
.wg-cap-tip {
|
||||
padding: 50px 20px 100px;
|
||||
font-size: 13px;
|
||||
color: #76839b;
|
||||
text-align: center;
|
||||
line-height: 180%;
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
}
|
||||
</style>
|
@ -52,6 +52,19 @@
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<div v-if="captchaEnabled && captchaShow">
|
||||
<GoCaptchaBtn
|
||||
v-model="captchaStatus"
|
||||
class="go-captcha-btn"
|
||||
width="100%"
|
||||
height="44px"
|
||||
:image-base64="captchaBase64"
|
||||
:thumb-base64="captchaThumbBase64"
|
||||
@confirm="handleConfirm"
|
||||
@refresh="handleRequestCaptCode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@ -84,8 +97,17 @@ import { validUsername, validPassword } from '@/utils/validate'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import type { ElForm } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref, watch, nextTick, reactive } from 'vue'
|
||||
import { MediaLoginUrl } from '@/api/user'
|
||||
import {
|
||||
MediaLoginUrl,
|
||||
GetCaptcha,
|
||||
CheckCaptcha,
|
||||
GetCaptchaConfig,
|
||||
} from '@/api/user'
|
||||
import GoCaptchaBtn from './components/GoCaptchaBtn.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { locale } = useI18n({ useScope: 'global' })
|
||||
const version = import.meta.env.VITE_APP_VERSION
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
@ -94,6 +116,7 @@ const loginForm = ref({
|
||||
account: import.meta.env.PROD === true ? '' : 'admin',
|
||||
password: import.meta.env.PROD === true ? '' : 'admin!@#',
|
||||
phrase: '',
|
||||
captchaKey: '',
|
||||
})
|
||||
const loginRules: InstanceType<typeof ElForm>['rules'] = {
|
||||
account: [
|
||||
@ -172,6 +195,64 @@ const mediaMap = reactive<Record<string, any>>({
|
||||
},
|
||||
})
|
||||
getMediaLoginUrl()
|
||||
getCaptchaConfig()
|
||||
|
||||
const captchaEnabled = ref(false)
|
||||
const captchaShow = ref(false)
|
||||
const captchaBase64 = ref('')
|
||||
const captchaThumbBase64 = ref('')
|
||||
const captchaKey = ref('')
|
||||
const captchaStatus = ref('default')
|
||||
const captchaAutoRefreshCount = ref(0)
|
||||
|
||||
function getCaptchaConfig() {
|
||||
new GetCaptchaConfig().request().then((response) => {
|
||||
captchaEnabled.value = response.data.enabled
|
||||
})
|
||||
}
|
||||
|
||||
function handleRequestCaptCode() {
|
||||
captchaBase64.value = ''
|
||||
captchaThumbBase64.value = ''
|
||||
captchaKey.value = ''
|
||||
|
||||
new GetCaptcha({ language: locale.value }).request().then((response) => {
|
||||
captchaBase64.value = response.data.base64
|
||||
captchaThumbBase64.value = response.data.thumbBase64
|
||||
captchaKey.value = response.data.key
|
||||
loginForm.value.captchaKey = response.data.key
|
||||
})
|
||||
}
|
||||
|
||||
function handleConfirm(dots: { x: number; y: number; index: number }[]) {
|
||||
if (dots.length < 1) {
|
||||
ElMessage.warning('please check the captcha')
|
||||
return
|
||||
}
|
||||
let dotArr = []
|
||||
for (const dot of dots) {
|
||||
dotArr.push(dot.x, dot.y)
|
||||
}
|
||||
|
||||
new CheckCaptcha({ dots: dotArr, key: captchaKey.value })
|
||||
.request()
|
||||
.then((response) => {
|
||||
ElMessage.success(`captcha passed`)
|
||||
captchaStatus.value = 'success'
|
||||
captchaAutoRefreshCount.value = 0
|
||||
})
|
||||
.catch(() => {
|
||||
if (captchaAutoRefreshCount.value > 5) {
|
||||
captchaAutoRefreshCount.value = 0
|
||||
captchaStatus.value = 'over'
|
||||
return
|
||||
}
|
||||
|
||||
handleRequestCaptCode()
|
||||
captchaAutoRefreshCount.value += 1
|
||||
captchaStatus.value = 'error'
|
||||
})
|
||||
}
|
||||
|
||||
const password = ref<HTMLInputElement>()
|
||||
function showPwd() {
|
||||
@ -214,6 +295,11 @@ function handleLogin() {
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
if (captchaEnabled.value) {
|
||||
captchaShow.value = true
|
||||
captchaStatus.value = 'default'
|
||||
loginForm.value.captchaKey = ''
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
return Promise.resolve(true)
|
||||
|
Loading…
Reference in New Issue
Block a user