Compare commits
No commits in common. "b43dcd8dab48cdd079847069c7d9e955e410db9e" and "62b8c6726194be611ff1945b78a7aecd95824512" have entirely different histories.
b43dcd8dab
...
62b8c67261
@ -20,7 +20,6 @@ type (
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
// Check user is exist request
|
// Check user is exist request
|
||||||
CheckUserRequest {
|
CheckUserRequest {
|
||||||
@ -43,7 +42,6 @@ type (
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
// User reset password request
|
// User reset password request
|
||||||
ResetPasswordRequest {
|
ResetPasswordRequest {
|
||||||
@ -57,7 +55,6 @@ type (
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
// Email login request
|
// Email login request
|
||||||
EmailLoginRequest {
|
EmailLoginRequest {
|
||||||
@ -97,7 +94,6 @@ type (
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
// Check user is exist request
|
// Check user is exist request
|
||||||
TelephoneCheckUserRequest {
|
TelephoneCheckUserRequest {
|
||||||
@ -122,7 +118,6 @@ type (
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
// User login response
|
// User login response
|
||||||
TelephoneResetPasswordRequest {
|
TelephoneResetPasswordRequest {
|
||||||
@ -137,7 +132,6 @@ type (
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
AppleLoginCallbackRequest {
|
AppleLoginCallbackRequest {
|
||||||
Code string `form:"code"`
|
Code string `form:"code"`
|
||||||
@ -159,16 +153,6 @@ type (
|
|||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
BlockImage string `json:"block_image,omitempty"`
|
|
||||||
}
|
|
||||||
SliderVerifyCaptchaRequest {
|
|
||||||
Id string `json:"id" validate:"required"`
|
|
||||||
X int `json:"x" validate:"required"`
|
|
||||||
Y int `json:"y" validate:"required"`
|
|
||||||
Trail string `json:"trail"`
|
|
||||||
}
|
|
||||||
SliderVerifyCaptchaResponse {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -207,24 +191,20 @@ service ppanel {
|
|||||||
get /check/telephone (TelephoneCheckUserRequest) returns (TelephoneCheckUserResponse)
|
get /check/telephone (TelephoneCheckUserRequest) returns (TelephoneCheckUserResponse)
|
||||||
|
|
||||||
@doc "User Telephone register"
|
@doc "User Telephone register"
|
||||||
@handler TelephoneRegister
|
@handler TelephoneUserRegister
|
||||||
post /register/telephone (TelephoneRegisterRequest) returns (LoginResponse)
|
post /register/telephone (TelephoneRegisterRequest) returns (LoginResponse)
|
||||||
|
|
||||||
@doc "Reset password by telephone"
|
@doc "Reset password"
|
||||||
@handler TelephoneResetPassword
|
@handler TelephoneResetPassword
|
||||||
post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse)
|
post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse)
|
||||||
|
|
||||||
@doc "Device Login"
|
|
||||||
@handler DeviceLogin
|
|
||||||
post /login/device (DeviceLoginRequest) returns (LoginResponse)
|
|
||||||
|
|
||||||
@doc "Generate captcha"
|
@doc "Generate captcha"
|
||||||
@handler GenerateCaptcha
|
@handler GenerateCaptcha
|
||||||
post /captcha/generate returns (GenerateCaptchaResponse)
|
post /captcha/generate returns (GenerateCaptchaResponse)
|
||||||
|
|
||||||
@doc "Verify slider captcha"
|
@doc "Device Login"
|
||||||
@handler SliderVerifyCaptcha
|
@handler DeviceLogin
|
||||||
post /captcha/slider/verify (SliderVerifyCaptchaRequest) returns (SliderVerifyCaptchaResponse)
|
post /login/device (DeviceLoginRequest) returns (LoginResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -244,10 +224,6 @@ service ppanel {
|
|||||||
@doc "Generate captcha"
|
@doc "Generate captcha"
|
||||||
@handler AdminGenerateCaptcha
|
@handler AdminGenerateCaptcha
|
||||||
post /captcha/generate returns (GenerateCaptchaResponse)
|
post /captcha/generate returns (GenerateCaptchaResponse)
|
||||||
|
|
||||||
@doc "Verify slider captcha"
|
|
||||||
@handler AdminSliderVerifyCaptcha
|
|
||||||
post /captcha/slider/verify (SliderVerifyCaptchaRequest) returns (SliderVerifyCaptchaResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -267,3 +243,4 @@ service ppanel {
|
|||||||
@handler AppleLoginCallback
|
@handler AppleLoginCallback
|
||||||
post /callback/apple (AppleLoginCallbackRequest)
|
post /callback/apple (AppleLoginCallbackRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/auth/admin"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify slider captcha
|
|
||||||
func AdminSliderVerifyCaptchaHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.SliderVerifyCaptchaRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := admin.NewAdminSliderVerifyCaptchaLogic(c.Request.Context(), svcCtx)
|
|
||||||
resp, err := l.AdminSliderVerifyCaptcha(&req)
|
|
||||||
result.HttpResult(c, resp, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/auth"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify slider captcha
|
|
||||||
func SliderVerifyCaptchaHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.SliderVerifyCaptchaRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := auth.NewSliderVerifyCaptchaLogic(c.Request.Context(), svcCtx)
|
|
||||||
resp, err := l.SliderVerifyCaptcha(&req)
|
|
||||||
result.HttpResult(c, resp, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/auth"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User Telephone register
|
|
||||||
func TelephoneRegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.TelephoneRegisterRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := auth.NewTelephoneRegisterLogic(c.Request.Context(), svcCtx)
|
|
||||||
resp, err := l.TelephoneRegister(&req)
|
|
||||||
result.HttpResult(c, resp, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -714,9 +714,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Generate captcha
|
// Generate captcha
|
||||||
authGroupRouter.POST("/captcha/generate", auth.GenerateCaptchaHandler(serverCtx))
|
authGroupRouter.POST("/captcha/generate", auth.GenerateCaptchaHandler(serverCtx))
|
||||||
|
|
||||||
// Verify slider captcha
|
|
||||||
authGroupRouter.POST("/captcha/slider/verify", auth.SliderVerifyCaptchaHandler(serverCtx))
|
|
||||||
|
|
||||||
// Check user is exist
|
// Check user is exist
|
||||||
authGroupRouter.GET("/check", auth.CheckUserHandler(serverCtx))
|
authGroupRouter.GET("/check", auth.CheckUserHandler(serverCtx))
|
||||||
|
|
||||||
@ -739,12 +736,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
authGroupRouter.POST("/register", auth.UserRegisterHandler(serverCtx))
|
authGroupRouter.POST("/register", auth.UserRegisterHandler(serverCtx))
|
||||||
|
|
||||||
// User Telephone register
|
// User Telephone register
|
||||||
authGroupRouter.POST("/register/telephone", auth.TelephoneRegisterHandler(serverCtx))
|
authGroupRouter.POST("/register/telephone", auth.TelephoneUserRegisterHandler(serverCtx))
|
||||||
|
|
||||||
// Reset password
|
// Reset password
|
||||||
authGroupRouter.POST("/reset", auth.ResetPasswordHandler(serverCtx))
|
authGroupRouter.POST("/reset", auth.ResetPasswordHandler(serverCtx))
|
||||||
|
|
||||||
// Reset password by telephone
|
// Reset password
|
||||||
authGroupRouter.POST("/reset/telephone", auth.TelephoneResetPasswordHandler(serverCtx))
|
authGroupRouter.POST("/reset/telephone", auth.TelephoneResetPasswordHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -755,9 +752,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Generate captcha
|
// Generate captcha
|
||||||
authAdminGroupRouter.POST("/captcha/generate", authAdmin.AdminGenerateCaptchaHandler(serverCtx))
|
authAdminGroupRouter.POST("/captcha/generate", authAdmin.AdminGenerateCaptchaHandler(serverCtx))
|
||||||
|
|
||||||
// Verify slider captcha
|
|
||||||
authAdminGroupRouter.POST("/captcha/slider/verify", authAdmin.AdminSliderVerifyCaptchaHandler(serverCtx))
|
|
||||||
|
|
||||||
// Admin login
|
// Admin login
|
||||||
authAdminGroupRouter.POST("/login", authAdmin.AdminLoginHandler(serverCtx))
|
authAdminGroupRouter.POST("/login", authAdmin.AdminLoginHandler(serverCtx))
|
||||||
|
|
||||||
@ -1120,10 +1114,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
serverGroupRouterV2 := router.Group("/v2/server")
|
serverV2GroupRouter := router.Group("/v2/server")
|
||||||
|
|
||||||
{
|
{
|
||||||
// Get Server Protocol Config
|
// Get Server Protocol Config
|
||||||
serverGroupRouterV2.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -632,7 +632,7 @@ func (l *RecalculateGroupLogic) executeTrafficGrouping(tx *gorm.DB, historyId in
|
|||||||
// 1. 获取所有设置了流量区间的节点组
|
// 1. 获取所有设置了流量区间的节点组
|
||||||
var nodeGroups []group.NodeGroup
|
var nodeGroups []group.NodeGroup
|
||||||
if err := tx.Where("for_calculation = ?", true).
|
if err := tx.Where("for_calculation = ?", true).
|
||||||
Where("max_traffic_gb > 0").
|
Where("(min_traffic_gb > 0 OR max_traffic_gb > 0)").
|
||||||
Find(&nodeGroups).Error; err != nil {
|
Find(&nodeGroups).Error; err != nil {
|
||||||
l.Errorw("failed to query node groups", logger.Field("error", err.Error()))
|
l.Errorw("failed to query node groups", logger.Field("error", err.Error()))
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|||||||
@ -64,17 +64,6 @@ func (l *AdminGenerateCaptchaLogic) AdminGenerateCaptcha() (resp *types.Generate
|
|||||||
} else if config.CaptchaType == "turnstile" {
|
} else if config.CaptchaType == "turnstile" {
|
||||||
// For Turnstile, just return the site key
|
// For Turnstile, just return the site key
|
||||||
resp.Id = config.TurnstileSiteKey
|
resp.Id = config.TurnstileSiteKey
|
||||||
} else if config.CaptchaType == "slider" {
|
|
||||||
// For slider, generate background and block images
|
|
||||||
sliderSvc := captcha.NewSliderService(l.svcCtx.Redis)
|
|
||||||
id, bgImage, blockImage, err := sliderSvc.GenerateSlider(l.ctx)
|
|
||||||
if err != nil {
|
|
||||||
l.Logger.Error("[AdminGenerateCaptchaLogic] Generate slider captcha error: ", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Generate slider captcha error: %v", err.Error())
|
|
||||||
}
|
|
||||||
resp.Id = id
|
|
||||||
resp.Image = bgImage
|
|
||||||
resp.BlockImage = blockImage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
|||||||
@ -137,28 +137,65 @@ func (l *AdminLoginLogic) AdminLogin(req *types.UserLoginRequest) (resp *types.L
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *AdminLoginLogic) verifyCaptcha(req *types.UserLoginRequest) error {
|
func (l *AdminLoginLogic) verifyCaptcha(req *types.UserLoginRequest) error {
|
||||||
|
// Get verify config from database
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[AdminLoginLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[AdminLoginLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var config struct {
|
||||||
CaptchaType string `json:"captcha_type"`
|
CaptchaType string `json:"captcha_type"`
|
||||||
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"`
|
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
}
|
}
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &cfg)
|
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
||||||
|
|
||||||
if !cfg.EnableAdminLoginCaptcha {
|
// Check if captcha is enabled for admin login
|
||||||
|
if !config.EnableAdminLoginCaptcha {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return captcha.VerifyCaptcha(l.ctx, l.svcCtx.Redis, cfg.CaptchaType, cfg.TurnstileSecret, captcha.VerifyInput{
|
// Verify based on captcha type
|
||||||
CaptchaId: req.CaptchaId,
|
if config.CaptchaType == "local" {
|
||||||
CaptchaCode: req.CaptchaCode,
|
if req.CaptchaId == "" || req.CaptchaCode == "" {
|
||||||
CfToken: req.CfToken,
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
SliderToken: req.SliderToken,
|
}
|
||||||
IP: req.IP,
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeLocal,
|
||||||
|
RedisClient: l.svcCtx.Redis,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CaptchaId, req.CaptchaCode, req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[AdminLoginLogic] Verify captcha error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
} else if config.CaptchaType == "turnstile" {
|
||||||
|
if req.CfToken == "" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeTurnstile,
|
||||||
|
TurnstileSecret: config.TurnstileSecret,
|
||||||
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CfToken, "", req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[AdminLoginLogic] Verify turnstile error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,28 +165,65 @@ func (l *AdminResetPasswordLogic) AdminResetPassword(req *types.ResetPasswordReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *AdminResetPasswordLogic) verifyCaptcha(req *types.ResetPasswordRequest) error {
|
func (l *AdminResetPasswordLogic) verifyCaptcha(req *types.ResetPasswordRequest) error {
|
||||||
|
// Get verify config from database
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[AdminResetPasswordLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[AdminResetPasswordLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var config struct {
|
||||||
CaptchaType string `json:"captcha_type"`
|
CaptchaType string `json:"captcha_type"`
|
||||||
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"`
|
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
}
|
}
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &cfg)
|
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
||||||
|
|
||||||
if !cfg.EnableAdminLoginCaptcha {
|
// Check if admin login captcha is enabled (use admin login captcha for reset password)
|
||||||
|
if !config.EnableAdminLoginCaptcha {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return captcha.VerifyCaptcha(l.ctx, l.svcCtx.Redis, cfg.CaptchaType, cfg.TurnstileSecret, captcha.VerifyInput{
|
// Verify based on captcha type
|
||||||
CaptchaId: req.CaptchaId,
|
if config.CaptchaType == "local" {
|
||||||
CaptchaCode: req.CaptchaCode,
|
if req.CaptchaId == "" || req.CaptchaCode == "" {
|
||||||
CfToken: req.CfToken,
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
SliderToken: req.SliderToken,
|
}
|
||||||
IP: req.IP,
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeLocal,
|
||||||
|
RedisClient: l.svcCtx.Redis,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CaptchaId, req.CaptchaCode, req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[AdminResetPasswordLogic] Verify captcha error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
} else if config.CaptchaType == "turnstile" {
|
||||||
|
if req.CfToken == "" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeTurnstile,
|
||||||
|
TurnstileSecret: config.TurnstileSecret,
|
||||||
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CfToken, "", req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[AdminResetPasswordLogic] Verify turnstile error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,57 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/captcha"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AdminSliderVerifyCaptchaLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify slider captcha
|
|
||||||
func NewAdminSliderVerifyCaptchaLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminSliderVerifyCaptchaLogic {
|
|
||||||
return &AdminSliderVerifyCaptchaLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *AdminSliderVerifyCaptchaLogic) AdminSliderVerifyCaptcha(req *types.SliderVerifyCaptchaRequest) (resp *types.SliderVerifyCaptchaResponse, err error) {
|
|
||||||
// Get verify config from database
|
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
|
||||||
if err != nil {
|
|
||||||
l.Logger.Error("[AdminSliderVerifyCaptchaLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var config struct {
|
|
||||||
CaptchaType string `json:"captcha_type"`
|
|
||||||
}
|
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
|
||||||
|
|
||||||
if config.CaptchaType != "slider" {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "slider captcha not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
sliderSvc := captcha.NewSliderService(l.svcCtx.Redis)
|
|
||||||
token, err := sliderSvc.VerifySlider(l.ctx, req.Id, req.X, req.Y, req.Trail)
|
|
||||||
if err != nil {
|
|
||||||
l.Logger.Error("[AdminSliderVerifyCaptchaLogic] VerifySlider error: ", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify slider error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.SliderVerifyCaptchaResponse{
|
|
||||||
Token: token,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -64,17 +64,6 @@ func (l *GenerateCaptchaLogic) GenerateCaptcha() (resp *types.GenerateCaptchaRes
|
|||||||
} else if config.CaptchaType == "turnstile" {
|
} else if config.CaptchaType == "turnstile" {
|
||||||
// For Turnstile, just return the site key
|
// For Turnstile, just return the site key
|
||||||
resp.Id = config.TurnstileSiteKey
|
resp.Id = config.TurnstileSiteKey
|
||||||
} else if config.CaptchaType == "slider" {
|
|
||||||
// For slider, generate background and block images
|
|
||||||
sliderSvc := captcha.NewSliderService(l.svcCtx.Redis)
|
|
||||||
id, bgImage, blockImage, err := sliderSvc.GenerateSlider(l.ctx)
|
|
||||||
if err != nil {
|
|
||||||
l.Logger.Error("[GenerateCaptchaLogic] Generate slider captcha error: ", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Generate slider captcha error: %v", err.Error())
|
|
||||||
}
|
|
||||||
resp.Id = id
|
|
||||||
resp.Image = bgImage
|
|
||||||
resp.BlockImage = blockImage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
|||||||
@ -163,28 +163,66 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *ResetPasswordLogic) verifyCaptcha(req *types.ResetPasswordRequest) error {
|
func (l *ResetPasswordLogic) verifyCaptcha(req *types.ResetPasswordRequest) error {
|
||||||
|
// Get verify config from database
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[ResetPasswordLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[ResetPasswordLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var config struct {
|
||||||
CaptchaType string `json:"captcha_type"`
|
CaptchaType string `json:"captcha_type"`
|
||||||
EnableUserResetPasswordCaptcha bool `json:"enable_user_reset_password_captcha"`
|
EnableUserResetPasswordCaptcha bool `json:"enable_user_reset_password_captcha"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
}
|
}
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &cfg)
|
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
||||||
|
|
||||||
if !cfg.EnableUserResetPasswordCaptcha {
|
// Check if user reset password captcha is enabled
|
||||||
|
if !config.EnableUserResetPasswordCaptcha {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return captcha.VerifyCaptcha(l.ctx, l.svcCtx.Redis, cfg.CaptchaType, cfg.TurnstileSecret, captcha.VerifyInput{
|
// Verify based on captcha type
|
||||||
CaptchaId: req.CaptchaId,
|
if config.CaptchaType == "local" {
|
||||||
CaptchaCode: req.CaptchaCode,
|
if req.CaptchaId == "" || req.CaptchaCode == "" {
|
||||||
CfToken: req.CfToken,
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
SliderToken: req.SliderToken,
|
}
|
||||||
IP: req.IP,
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeLocal,
|
||||||
|
RedisClient: l.svcCtx.Redis,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CaptchaId, req.CaptchaCode, req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[ResetPasswordLogic] Verify captcha error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
} else if config.CaptchaType == "turnstile" {
|
||||||
|
if req.CfToken == "" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeTurnstile,
|
||||||
|
TurnstileSecret: config.TurnstileSecret,
|
||||||
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CfToken, "", req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[ResetPasswordLogic] Verify turnstile error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/captcha"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SliderVerifyCaptchaLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify slider captcha
|
|
||||||
func NewSliderVerifyCaptchaLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SliderVerifyCaptchaLogic {
|
|
||||||
return &SliderVerifyCaptchaLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SliderVerifyCaptchaLogic) SliderVerifyCaptcha(req *types.SliderVerifyCaptchaRequest) (resp *types.SliderVerifyCaptchaResponse, err error) {
|
|
||||||
var config struct {
|
|
||||||
CaptchaType string `json:"captcha_type"`
|
|
||||||
}
|
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
|
||||||
}
|
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
|
||||||
|
|
||||||
if config.CaptchaType != string(captcha.CaptchaTypeSlider) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "slider captcha not enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
sliderSvc := captcha.NewSliderService(l.svcCtx.Redis)
|
|
||||||
token, err := sliderSvc.VerifySlider(l.ctx, req.Id, req.X, req.Y, req.Trail)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "slider verify failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.SliderVerifyCaptchaResponse{Token: token}, nil
|
|
||||||
}
|
|
||||||
@ -172,28 +172,65 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *TelephoneLoginLogic) verifyCaptcha(req *types.TelephoneLoginRequest) error {
|
func (l *TelephoneLoginLogic) verifyCaptcha(req *types.TelephoneLoginRequest) error {
|
||||||
|
// Get verify config from database
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[TelephoneLoginLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[TelephoneLoginLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var config struct {
|
||||||
CaptchaType string `json:"captcha_type"`
|
CaptchaType string `json:"captcha_type"`
|
||||||
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"`
|
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
}
|
}
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &cfg)
|
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
||||||
|
|
||||||
if !cfg.EnableUserLoginCaptcha {
|
// Check if captcha is enabled for user login
|
||||||
|
if !config.EnableUserLoginCaptcha {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return captcha.VerifyCaptcha(l.ctx, l.svcCtx.Redis, cfg.CaptchaType, cfg.TurnstileSecret, captcha.VerifyInput{
|
// Verify based on captcha type
|
||||||
CaptchaId: req.CaptchaId,
|
if config.CaptchaType == "local" {
|
||||||
CaptchaCode: req.CaptchaCode,
|
if req.CaptchaId == "" || req.CaptchaCode == "" {
|
||||||
CfToken: req.CfToken,
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
SliderToken: req.SliderToken,
|
}
|
||||||
IP: req.IP,
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeLocal,
|
||||||
|
RedisClient: l.svcCtx.Redis,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CaptchaId, req.CaptchaCode, req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[TelephoneLoginLogic] Verify captcha error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
} else if config.CaptchaType == "turnstile" {
|
||||||
|
if req.CfToken == "" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeTurnstile,
|
||||||
|
TurnstileSecret: config.TurnstileSecret,
|
||||||
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CfToken, "", req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[TelephoneLoginLogic] Verify turnstile error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TelephoneRegisterLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// User Telephone register
|
|
||||||
func NewTelephoneRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TelephoneRegisterLogic {
|
|
||||||
return &TelephoneRegisterLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *TelephoneRegisterLogic) TelephoneRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) {
|
|
||||||
// todo: add your logic here and delete this line
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@ -288,28 +288,66 @@ func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *TelephoneUserRegisterLogic) verifyCaptcha(req *types.TelephoneRegisterRequest) error {
|
func (l *TelephoneUserRegisterLogic) verifyCaptcha(req *types.TelephoneRegisterRequest) error {
|
||||||
|
// Get verify config from database
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[TelephoneUserRegisterLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[TelephoneUserRegisterLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var config struct {
|
||||||
CaptchaType string `json:"captcha_type"`
|
CaptchaType string `json:"captcha_type"`
|
||||||
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"`
|
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
}
|
}
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &cfg)
|
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
||||||
|
|
||||||
if !cfg.EnableUserRegisterCaptcha {
|
// Check if captcha is enabled for user register
|
||||||
|
if !config.EnableUserRegisterCaptcha {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return captcha.VerifyCaptcha(l.ctx, l.svcCtx.Redis, cfg.CaptchaType, cfg.TurnstileSecret, captcha.VerifyInput{
|
// Verify based on captcha type
|
||||||
CaptchaId: req.CaptchaId,
|
if config.CaptchaType == "local" {
|
||||||
CaptchaCode: req.CaptchaCode,
|
if req.CaptchaId == "" || req.CaptchaCode == "" {
|
||||||
CfToken: req.CfToken,
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
SliderToken: req.SliderToken,
|
}
|
||||||
IP: req.IP,
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeLocal,
|
||||||
|
RedisClient: l.svcCtx.Redis,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CaptchaId, req.CaptchaCode, req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[TelephoneUserRegisterLogic] Verify captcha error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
} else if config.CaptchaType == "turnstile" {
|
||||||
|
if req.CfToken == "" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeTurnstile,
|
||||||
|
TurnstileSecret: config.TurnstileSecret,
|
||||||
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CfToken, "", req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[TelephoneUserRegisterLogic] Verify turnstile error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -142,28 +142,65 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *UserLoginLogic) verifyCaptcha(req *types.UserLoginRequest) error {
|
func (l *UserLoginLogic) verifyCaptcha(req *types.UserLoginRequest) error {
|
||||||
|
// Get verify config from database
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLoginLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLoginLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var config struct {
|
||||||
CaptchaType string `json:"captcha_type"`
|
CaptchaType string `json:"captcha_type"`
|
||||||
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"`
|
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
}
|
}
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &cfg)
|
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
||||||
|
|
||||||
if !cfg.EnableUserLoginCaptcha {
|
// Check if captcha is enabled for user login
|
||||||
|
if !config.EnableUserLoginCaptcha {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return captcha.VerifyCaptcha(l.ctx, l.svcCtx.Redis, cfg.CaptchaType, cfg.TurnstileSecret, captcha.VerifyInput{
|
// Verify based on captcha type
|
||||||
CaptchaId: req.CaptchaId,
|
if config.CaptchaType == "local" {
|
||||||
CaptchaCode: req.CaptchaCode,
|
if req.CaptchaId == "" || req.CaptchaCode == "" {
|
||||||
CfToken: req.CfToken,
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
SliderToken: req.SliderToken,
|
}
|
||||||
IP: req.IP,
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeLocal,
|
||||||
|
RedisClient: l.svcCtx.Redis,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CaptchaId, req.CaptchaCode, req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[UserLoginLogic] Verify captcha error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
} else if config.CaptchaType == "turnstile" {
|
||||||
|
if req.CfToken == "" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeTurnstile,
|
||||||
|
TurnstileSecret: config.TurnstileSecret,
|
||||||
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CfToken, "", req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[UserLoginLogic] Verify turnstile error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -339,28 +339,65 @@ func (l *UserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *UserRegisterLogic) verifyCaptcha(req *types.UserRegisterRequest) error {
|
func (l *UserRegisterLogic) verifyCaptcha(req *types.UserRegisterRequest) error {
|
||||||
|
// Get verify config from database
|
||||||
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserRegisterLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserRegisterLogic] GetVerifyConfig error: ", logger.Field("error", err.Error()))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var config struct {
|
||||||
CaptchaType string `json:"captcha_type"`
|
CaptchaType string `json:"captcha_type"`
|
||||||
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"`
|
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
}
|
}
|
||||||
tool.SystemConfigSliceReflectToStruct(verifyCfg, &cfg)
|
tool.SystemConfigSliceReflectToStruct(verifyCfg, &config)
|
||||||
|
|
||||||
if !cfg.EnableUserRegisterCaptcha {
|
// Check if user register captcha is enabled
|
||||||
|
if !config.EnableUserRegisterCaptcha {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return captcha.VerifyCaptcha(l.ctx, l.svcCtx.Redis, cfg.CaptchaType, cfg.TurnstileSecret, captcha.VerifyInput{
|
// Verify based on captcha type
|
||||||
CaptchaId: req.CaptchaId,
|
if config.CaptchaType == "local" {
|
||||||
CaptchaCode: req.CaptchaCode,
|
if req.CaptchaId == "" || req.CaptchaCode == "" {
|
||||||
CfToken: req.CfToken,
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
SliderToken: req.SliderToken,
|
}
|
||||||
IP: req.IP,
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeLocal,
|
||||||
|
RedisClient: l.svcCtx.Redis,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CaptchaId, req.CaptchaCode, req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[UserRegisterLogic] Verify captcha error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
} else if config.CaptchaType == "turnstile" {
|
||||||
|
if req.CfToken == "" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
||||||
|
}
|
||||||
|
|
||||||
|
captchaService := captcha.NewService(captcha.Config{
|
||||||
|
Type: captcha.CaptchaTypeTurnstile,
|
||||||
|
TurnstileSecret: config.TurnstileSecret,
|
||||||
|
})
|
||||||
|
|
||||||
|
valid, err := captchaService.Verify(l.ctx, req.CfToken, "", req.IP)
|
||||||
|
if err != nil {
|
||||||
|
l.Logger.Error("[UserRegisterLogic] Verify turnstile error: ", logger.Field("error", err.Error()))
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -893,7 +893,6 @@ type GenerateCaptchaResponse struct {
|
|||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
BlockImage string `json:"block_image,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAdsDetailRequest struct {
|
type GetAdsDetailRequest struct {
|
||||||
@ -2321,7 +2320,6 @@ type ResetPasswordRequest struct {
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResetSortRequest struct {
|
type ResetSortRequest struct {
|
||||||
@ -2553,17 +2551,6 @@ type SiteCustomDataContacts struct {
|
|||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SliderVerifyCaptchaRequest struct {
|
|
||||||
Id string `json:"id" validate:"required"`
|
|
||||||
X int `json:"x" validate:"required"`
|
|
||||||
Y int `json:"y" validate:"required"`
|
|
||||||
Trail string `json:"trail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SliderVerifyCaptchaResponse struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortItem struct {
|
type SortItem struct {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
Sort int64 `json:"sort" validate:"required"`
|
Sort int64 `json:"sort" validate:"required"`
|
||||||
@ -2715,7 +2702,6 @@ type TelephoneLoginRequest struct {
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelephoneRegisterRequest struct {
|
type TelephoneRegisterRequest struct {
|
||||||
@ -2731,7 +2717,6 @@ type TelephoneRegisterRequest struct {
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelephoneResetPasswordRequest struct {
|
type TelephoneResetPasswordRequest struct {
|
||||||
@ -2746,7 +2731,6 @@ type TelephoneResetPasswordRequest struct {
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestEmailSendRequest struct {
|
type TestEmailSendRequest struct {
|
||||||
@ -3217,7 +3201,6 @@ type UserLoginRequest struct {
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRegisterRequest struct {
|
type UserRegisterRequest struct {
|
||||||
@ -3232,7 +3215,6 @@ type UserRegisterRequest struct {
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CaptchaId string `json:"captcha_id,optional"`
|
CaptchaId string `json:"captcha_id,optional"`
|
||||||
CaptchaCode string `json:"captcha_code,optional"`
|
CaptchaCode string `json:"captcha_code,optional"`
|
||||||
SliderToken string `json:"slider_token,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserStatistics struct {
|
type UserStatistics struct {
|
||||||
|
|||||||
@ -11,7 +11,6 @@ type CaptchaType string
|
|||||||
const (
|
const (
|
||||||
CaptchaTypeLocal CaptchaType = "local"
|
CaptchaTypeLocal CaptchaType = "local"
|
||||||
CaptchaTypeTurnstile CaptchaType = "turnstile"
|
CaptchaTypeTurnstile CaptchaType = "turnstile"
|
||||||
CaptchaTypeSlider CaptchaType = "slider"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service defines the captcha service interface
|
// Service defines the captcha service interface
|
||||||
@ -19,30 +18,17 @@ type Service interface {
|
|||||||
// Generate generates a new captcha
|
// Generate generates a new captcha
|
||||||
// For local captcha: returns id and base64 image
|
// For local captcha: returns id and base64 image
|
||||||
// For turnstile: returns empty strings
|
// For turnstile: returns empty strings
|
||||||
// For slider: returns id, background image, and block image (in image field as JSON)
|
|
||||||
Generate(ctx context.Context) (id string, image string, err error)
|
Generate(ctx context.Context) (id string, image string, err error)
|
||||||
|
|
||||||
// Verify verifies the captcha
|
// Verify verifies the captcha
|
||||||
// For local captcha: token is captcha id, code is user input
|
// For local captcha: token is captcha id, code is user input
|
||||||
// For turnstile: token is cf-turnstile-response, code is ignored
|
// For turnstile: token is cf-turnstile-response, code is ignored
|
||||||
// For slider: use VerifySlider instead
|
|
||||||
Verify(ctx context.Context, token string, code string, ip string) (bool, error)
|
Verify(ctx context.Context, token string, code string, ip string) (bool, error)
|
||||||
|
|
||||||
// GetType returns the captcha type
|
// GetType returns the captcha type
|
||||||
GetType() CaptchaType
|
GetType() CaptchaType
|
||||||
}
|
}
|
||||||
|
|
||||||
// SliderService extends Service with slider-specific verification
|
|
||||||
type SliderService interface {
|
|
||||||
Service
|
|
||||||
// VerifySlider verifies slider position and trail, returns a one-time token on success
|
|
||||||
VerifySlider(ctx context.Context, id string, x, y int, trail string) (token string, err error)
|
|
||||||
// VerifySliderToken verifies the one-time token issued after slider verification
|
|
||||||
VerifySliderToken(ctx context.Context, token string) (bool, error)
|
|
||||||
// GenerateSlider returns id, background image base64, block image base64
|
|
||||||
GenerateSlider(ctx context.Context) (id string, bgImage string, blockImage string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config holds the configuration for captcha service
|
// Config holds the configuration for captcha service
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Type CaptchaType
|
Type CaptchaType
|
||||||
@ -55,16 +41,9 @@ func NewService(config Config) Service {
|
|||||||
switch config.Type {
|
switch config.Type {
|
||||||
case CaptchaTypeTurnstile:
|
case CaptchaTypeTurnstile:
|
||||||
return newTurnstileService(config.TurnstileSecret)
|
return newTurnstileService(config.TurnstileSecret)
|
||||||
case CaptchaTypeSlider:
|
|
||||||
return newSliderService(config.RedisClient)
|
|
||||||
case CaptchaTypeLocal:
|
case CaptchaTypeLocal:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
return newLocalService(config.RedisClient)
|
return newLocalService(config.RedisClient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSliderService creates a slider captcha service
|
|
||||||
func NewSliderService(redisClient *redis.Client) SliderService {
|
|
||||||
return newSliderService(redisClient)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,515 +0,0 @@
|
|||||||
package captcha
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/png"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
sliderBgWidth = 560
|
|
||||||
sliderBgHeight = 280
|
|
||||||
sliderBlockSize = 100
|
|
||||||
sliderMinX = 140
|
|
||||||
sliderMaxX = 420
|
|
||||||
sliderTolerance = 6
|
|
||||||
sliderExpiry = 5 * time.Minute
|
|
||||||
sliderTokenExpiry = 30 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
type sliderShape int
|
|
||||||
|
|
||||||
const (
|
|
||||||
shapeSquare sliderShape = 0
|
|
||||||
shapeCircle sliderShape = 1
|
|
||||||
shapeDiamond sliderShape = 2
|
|
||||||
shapeStar sliderShape = 3
|
|
||||||
shapeTriangle sliderShape = 4
|
|
||||||
shapeTrapezoid sliderShape = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
type sliderService struct {
|
|
||||||
redis *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSliderService(redisClient *redis.Client) *sliderService {
|
|
||||||
return &sliderService{redis: redisClient}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sliderData stores the correct position and shape in Redis
|
|
||||||
type sliderData struct {
|
|
||||||
X int `json:"x"`
|
|
||||||
Y int `json:"y"`
|
|
||||||
Shape sliderShape `json:"shape"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// inMask returns true if pixel (dx,dy) within the block bounding box belongs to the shape
|
|
||||||
func inMask(dx, dy int, shape sliderShape) bool {
|
|
||||||
half := sliderBlockSize / 2
|
|
||||||
switch shape {
|
|
||||||
case shapeCircle:
|
|
||||||
ex := dx - half
|
|
||||||
ey := dy - half
|
|
||||||
return ex*ex+ey*ey <= half*half
|
|
||||||
case shapeDiamond:
|
|
||||||
return abs(dx-half)+abs(dy-half) <= half
|
|
||||||
case shapeStar:
|
|
||||||
return inStar(dx, dy, half)
|
|
||||||
case shapeTriangle:
|
|
||||||
return inTriangle(dx, dy)
|
|
||||||
case shapeTrapezoid:
|
|
||||||
return inTrapezoid(dx, dy)
|
|
||||||
default: // shapeSquare
|
|
||||||
margin := 8
|
|
||||||
return dx >= margin && dx < sliderBlockSize-margin && dy >= margin && dy < sliderBlockSize-margin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func abs(v int) int {
|
|
||||||
if v < 0 {
|
|
||||||
return -v
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// pointInPolygon uses ray-casting to test if (x,y) is inside the polygon defined by pts.
|
|
||||||
func pointInPolygon(x, y float64, pts [][2]float64) bool {
|
|
||||||
n := len(pts)
|
|
||||||
inside := false
|
|
||||||
j := n - 1
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
xi, yi := pts[i][0], pts[i][1]
|
|
||||||
xj, yj := pts[j][0], pts[j][1]
|
|
||||||
if ((yi > y) != (yj > y)) && (x < (xj-xi)*(y-yi)/(yj-yi)+xi) {
|
|
||||||
inside = !inside
|
|
||||||
}
|
|
||||||
j = i
|
|
||||||
}
|
|
||||||
return inside
|
|
||||||
}
|
|
||||||
|
|
||||||
// inStar returns true if (dx,dy) is inside a 5-pointed star centered in the block.
|
|
||||||
func inStar(dx, dy, half int) bool {
|
|
||||||
cx, cy := float64(half), float64(half)
|
|
||||||
r1 := float64(half) * 0.92 // outer radius
|
|
||||||
r2 := float64(half) * 0.40 // inner radius
|
|
||||||
x := float64(dx) - cx
|
|
||||||
y := float64(dy) - cy
|
|
||||||
pts := make([][2]float64, 10)
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
angle := float64(i)*math.Pi/5 - math.Pi/2
|
|
||||||
r := r1
|
|
||||||
if i%2 == 1 {
|
|
||||||
r = r2
|
|
||||||
}
|
|
||||||
pts[i] = [2]float64{r * math.Cos(angle), r * math.Sin(angle)}
|
|
||||||
}
|
|
||||||
return pointInPolygon(x, y, pts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// inTriangle returns true if (dx,dy) is inside an upward-pointing triangle.
|
|
||||||
func inTriangle(dx, dy int) bool {
|
|
||||||
margin := 5
|
|
||||||
size := sliderBlockSize - 2*margin
|
|
||||||
half := float64(sliderBlockSize) / 2
|
|
||||||
ax, ay := half, float64(margin)
|
|
||||||
bx, by := float64(margin), float64(margin+size)
|
|
||||||
cx, cy2 := float64(margin+size), float64(margin+size)
|
|
||||||
px, py := float64(dx), float64(dy)
|
|
||||||
d1 := (px-bx)*(ay-by) - (ax-bx)*(py-by)
|
|
||||||
d2 := (px-cx)*(by-cy2) - (bx-cx)*(py-cy2)
|
|
||||||
d3 := (px-ax)*(cy2-ay) - (cx-ax)*(py-ay)
|
|
||||||
hasNeg := (d1 < 0) || (d2 < 0) || (d3 < 0)
|
|
||||||
hasPos := (d1 > 0) || (d2 > 0) || (d3 > 0)
|
|
||||||
return !(hasNeg && hasPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// inTrapezoid returns true if (dx,dy) is inside a trapezoid (wider at bottom).
|
|
||||||
func inTrapezoid(dx, dy int) bool {
|
|
||||||
margin := 5
|
|
||||||
topY := float64(margin)
|
|
||||||
bottomY := float64(sliderBlockSize - margin)
|
|
||||||
totalH := bottomY - topY
|
|
||||||
half := float64(sliderBlockSize) / 2
|
|
||||||
topHalfW := float64(sliderBlockSize) * 0.25
|
|
||||||
bottomHalfW := float64(sliderBlockSize) * 0.45
|
|
||||||
x, y := float64(dx), float64(dy)
|
|
||||||
if y < topY || y > bottomY {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
t := (y - topY) / totalH
|
|
||||||
hw := topHalfW + t*(bottomHalfW-topHalfW)
|
|
||||||
return x >= half-hw && x <= half+hw
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sliderService) GenerateSlider(ctx context.Context) (id string, bgImage string, blockImage string, err error) {
|
|
||||||
bg := generateBackground()
|
|
||||||
|
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
x := sliderMinX + r.Intn(sliderMaxX-sliderMinX)
|
|
||||||
y := r.Intn(sliderBgHeight - sliderBlockSize)
|
|
||||||
shape := sliderShape(r.Intn(6))
|
|
||||||
|
|
||||||
block := cropBlockShaped(bg, x, y, shape)
|
|
||||||
cutBackgroundShaped(bg, x, y, shape)
|
|
||||||
|
|
||||||
bgB64, err := imageToPNGBase64(bg)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", "", err
|
|
||||||
}
|
|
||||||
blockB64, err := imageToPNGBase64(block)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
id = uuid.New().String()
|
|
||||||
data, _ := json.Marshal(sliderData{X: x, Y: y, Shape: shape})
|
|
||||||
key := fmt.Sprintf("captcha:slider:%s", id)
|
|
||||||
if err = s.redis.Set(ctx, key, string(data), sliderExpiry).Err(); err != nil {
|
|
||||||
return "", "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, bgB64, blockB64, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sliderService) Generate(ctx context.Context) (id string, image string, err error) {
|
|
||||||
id, _, _, err = s.GenerateSlider(ctx)
|
|
||||||
return id, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrailPoint records a pointer position and timestamp during drag
|
|
||||||
type TrailPoint struct {
|
|
||||||
X int `json:"x"`
|
|
||||||
Y int `json:"y"`
|
|
||||||
T int64 `json:"t"` // milliseconds since drag start
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateTrail performs human-behaviour checks on the drag trail.
|
|
||||||
//
|
|
||||||
// Rules:
|
|
||||||
// 1. Trail must be provided and have >= 8 points
|
|
||||||
// 2. Total drag duration: 300ms – 15000ms
|
|
||||||
// 3. First point x <= 10 (started from left)
|
|
||||||
// 4. No single-step jump > 80px
|
|
||||||
// 5. Final x within tolerance*2 of declared x
|
|
||||||
// 6. Speed variance > 0 (not perfectly uniform / robotic)
|
|
||||||
// 7. Y-axis total deviation >= 2px (path is not a perfect horizontal line)
|
|
||||||
func validateTrail(trail []TrailPoint, declaredX int) bool {
|
|
||||||
if len(trail) < 8 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := trail[len(trail)-1].T - trail[0].T
|
|
||||||
if duration < 300 || duration > 15000 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if trail[0].X > 10 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect per-step speeds and check max jump
|
|
||||||
var speeds []float64
|
|
||||||
for i := 1; i < len(trail); i++ {
|
|
||||||
dt := float64(trail[i].T - trail[i-1].T)
|
|
||||||
dx := float64(trail[i].X - trail[i-1].X)
|
|
||||||
dy := float64(trail[i].Y - trail[i-1].Y)
|
|
||||||
if abs(int(dx)) > 80 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if dt > 0 {
|
|
||||||
dist := math.Sqrt(dx*dx + dy*dy)
|
|
||||||
speeds = append(speeds, dist/dt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speed variance check – robot drag tends to be perfectly uniform
|
|
||||||
if len(speeds) >= 3 {
|
|
||||||
mean := 0.0
|
|
||||||
for _, v := range speeds {
|
|
||||||
mean += v
|
|
||||||
}
|
|
||||||
mean /= float64(len(speeds))
|
|
||||||
variance := 0.0
|
|
||||||
for _, v := range speeds {
|
|
||||||
d := v - mean
|
|
||||||
variance += d * d
|
|
||||||
}
|
|
||||||
variance /= float64(len(speeds))
|
|
||||||
// If variance is essentially 0, it's robotic
|
|
||||||
if variance < 1e-6 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Y-axis deviation: humans almost always move slightly on Y
|
|
||||||
minY := trail[0].Y
|
|
||||||
maxY := trail[0].Y
|
|
||||||
for _, p := range trail {
|
|
||||||
if p.Y < minY {
|
|
||||||
minY = p.Y
|
|
||||||
}
|
|
||||||
if p.Y > maxY {
|
|
||||||
maxY = p.Y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if maxY-minY < 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final position check
|
|
||||||
lastX := trail[len(trail)-1].X
|
|
||||||
if diff := abs(lastX - declaredX); diff > sliderTolerance*2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sliderService) VerifySlider(ctx context.Context, id string, x, y int, trail string) (token string, err error) {
|
|
||||||
// Trail is mandatory
|
|
||||||
if trail == "" {
|
|
||||||
return "", fmt.Errorf("trail required")
|
|
||||||
}
|
|
||||||
var points []TrailPoint
|
|
||||||
if jsonErr := json.Unmarshal([]byte(trail), &points); jsonErr != nil {
|
|
||||||
return "", fmt.Errorf("invalid trail")
|
|
||||||
}
|
|
||||||
if !validateTrail(points, x) {
|
|
||||||
return "", fmt.Errorf("trail validation failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
key := fmt.Sprintf("captcha:slider:%s", id)
|
|
||||||
val, err := s.redis.Get(ctx, key).Result()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("captcha not found or expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
var data sliderData
|
|
||||||
if err = json.Unmarshal([]byte(val), &data); err != nil {
|
|
||||||
return "", fmt.Errorf("invalid captcha data")
|
|
||||||
}
|
|
||||||
|
|
||||||
diffX := abs(x - data.X)
|
|
||||||
diffY := abs(y - data.Y)
|
|
||||||
if diffX > sliderTolerance || diffY > sliderTolerance {
|
|
||||||
s.redis.Del(ctx, key)
|
|
||||||
return "", fmt.Errorf("position mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.redis.Del(ctx, key)
|
|
||||||
|
|
||||||
sliderToken := uuid.New().String()
|
|
||||||
tokenKey := fmt.Sprintf("captcha:slider:token:%s", sliderToken)
|
|
||||||
if err = s.redis.Set(ctx, tokenKey, "1", sliderTokenExpiry).Err(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return sliderToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sliderService) VerifySliderToken(ctx context.Context, token string) (bool, error) {
|
|
||||||
if token == "" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
tokenKey := fmt.Sprintf("captcha:slider:token:%s", token)
|
|
||||||
val, err := s.redis.Get(ctx, tokenKey).Result()
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if val != "1" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
s.redis.Del(ctx, tokenKey)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sliderService) Verify(ctx context.Context, token string, code string, ip string) (bool, error) {
|
|
||||||
return s.VerifySliderToken(ctx, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sliderService) GetType() CaptchaType {
|
|
||||||
return CaptchaTypeSlider
|
|
||||||
}
|
|
||||||
|
|
||||||
// cropBlockShaped copies pixels within the shape mask from bg into a new block image.
|
|
||||||
// Pixels outside the mask are transparent. A 2-pixel white border is drawn along the shape edge.
|
|
||||||
func cropBlockShaped(bg *image.NRGBA, x, y int, shape sliderShape) *image.NRGBA {
|
|
||||||
block := image.NewNRGBA(image.Rect(0, 0, sliderBlockSize, sliderBlockSize))
|
|
||||||
for dy := 0; dy < sliderBlockSize; dy++ {
|
|
||||||
for dx := 0; dx < sliderBlockSize; dx++ {
|
|
||||||
if inMask(dx, dy, shape) {
|
|
||||||
block.SetNRGBA(dx, dy, bg.NRGBAAt(x+dx, y+dy))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw 2-pixel bright border along shape edge
|
|
||||||
borderColor := color.NRGBA{R: 255, G: 255, B: 255, A: 230}
|
|
||||||
for dy := 0; dy < sliderBlockSize; dy++ {
|
|
||||||
for dx := 0; dx < sliderBlockSize; dx++ {
|
|
||||||
if !inMask(dx, dy, shape) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nearEdge := false
|
|
||||||
check:
|
|
||||||
for ddy := -2; ddy <= 2; ddy++ {
|
|
||||||
for ddx := -2; ddx <= 2; ddx++ {
|
|
||||||
if abs(ddx)+abs(ddy) > 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nx, ny := dx+ddx, dy+ddy
|
|
||||||
if nx < 0 || nx >= sliderBlockSize || ny < 0 || ny >= sliderBlockSize || !inMask(nx, ny, shape) {
|
|
||||||
nearEdge = true
|
|
||||||
break check
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nearEdge {
|
|
||||||
block.SetNRGBA(dx, dy, borderColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
// cutBackgroundShaped blanks the shape area and draws a border outline
|
|
||||||
func cutBackgroundShaped(bg *image.NRGBA, x, y int, shape sliderShape) {
|
|
||||||
holeColor := color.NRGBA{R: 0, G: 0, B: 0, A: 100}
|
|
||||||
borderColor := color.NRGBA{R: 255, G: 255, B: 255, A: 220}
|
|
||||||
|
|
||||||
// Fill hole
|
|
||||||
for dy := 0; dy < sliderBlockSize; dy++ {
|
|
||||||
for dx := 0; dx < sliderBlockSize; dx++ {
|
|
||||||
if inMask(dx, dy, shape) {
|
|
||||||
bg.SetNRGBA(x+dx, y+dy, holeColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw 2-pixel border along hole edge
|
|
||||||
for dy := 0; dy < sliderBlockSize; dy++ {
|
|
||||||
for dx := 0; dx < sliderBlockSize; dx++ {
|
|
||||||
if !inMask(dx, dy, shape) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nearEdge := false
|
|
||||||
check:
|
|
||||||
for ddy := -2; ddy <= 2; ddy++ {
|
|
||||||
for ddx := -2; ddx <= 2; ddx++ {
|
|
||||||
if abs(ddx)+abs(ddy) > 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nx, ny := dx+ddx, dy+ddy
|
|
||||||
if nx < 0 || nx >= sliderBlockSize || ny < 0 || ny >= sliderBlockSize || !inMask(nx, ny, shape) {
|
|
||||||
nearEdge = true
|
|
||||||
break check
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nearEdge {
|
|
||||||
bg.SetNRGBA(x+dx, y+dy, borderColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateBackground creates a colorful 320x160 background image
|
|
||||||
func generateBackground() *image.NRGBA {
|
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
img := image.NewNRGBA(image.Rect(0, 0, sliderBgWidth, sliderBgHeight))
|
|
||||||
|
|
||||||
blockW := 60
|
|
||||||
blockH := 60
|
|
||||||
palette := []color.NRGBA{
|
|
||||||
{R: 70, G: 130, B: 180, A: 255},
|
|
||||||
{R: 60, G: 179, B: 113, A: 255},
|
|
||||||
{R: 205, G: 92, B: 92, A: 255},
|
|
||||||
{R: 255, G: 165, B: 0, A: 255},
|
|
||||||
{R: 147, G: 112, B: 219, A: 255},
|
|
||||||
{R: 64, G: 224, B: 208, A: 255},
|
|
||||||
{R: 220, G: 120, B: 60, A: 255},
|
|
||||||
{R: 100, G: 149, B: 237, A: 255},
|
|
||||||
}
|
|
||||||
|
|
||||||
for by := 0; by*blockH < sliderBgHeight; by++ {
|
|
||||||
for bx := 0; bx*blockW < sliderBgWidth; bx++ {
|
|
||||||
base := palette[r.Intn(len(palette))]
|
|
||||||
x0 := bx * blockW
|
|
||||||
y0 := by * blockH
|
|
||||||
x1 := x0 + blockW
|
|
||||||
y1 := y0 + blockH
|
|
||||||
for py := y0; py < y1 && py < sliderBgHeight; py++ {
|
|
||||||
for px := x0; px < x1 && px < sliderBgWidth; px++ {
|
|
||||||
v := int8(r.Intn(41) - 20)
|
|
||||||
img.SetNRGBA(px, py, color.NRGBA{
|
|
||||||
R: addVariation(base.R, v),
|
|
||||||
G: addVariation(base.G, v),
|
|
||||||
B: addVariation(base.B, v),
|
|
||||||
A: 255,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add some random circles for visual complexity
|
|
||||||
numCircles := 6 + r.Intn(6)
|
|
||||||
for i := 0; i < numCircles; i++ {
|
|
||||||
cx := r.Intn(sliderBgWidth)
|
|
||||||
cy := r.Intn(sliderBgHeight)
|
|
||||||
radius := 18 + r.Intn(30)
|
|
||||||
circleColor := color.NRGBA{
|
|
||||||
R: uint8(r.Intn(256)),
|
|
||||||
G: uint8(r.Intn(256)),
|
|
||||||
B: uint8(r.Intn(256)),
|
|
||||||
A: 180,
|
|
||||||
}
|
|
||||||
drawCircle(img, cx, cy, radius, circleColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
return img
|
|
||||||
}
|
|
||||||
|
|
||||||
func addVariation(base uint8, v int8) uint8 {
|
|
||||||
result := int(base) + int(v)
|
|
||||||
if result < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if result > 255 {
|
|
||||||
return 255
|
|
||||||
}
|
|
||||||
return uint8(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawCircle(img *image.NRGBA, cx, cy, radius int, c color.NRGBA) {
|
|
||||||
bounds := img.Bounds()
|
|
||||||
for y := cy - radius; y <= cy+radius; y++ {
|
|
||||||
for x := cx - radius; x <= cx+radius; x++ {
|
|
||||||
if (x-cx)*(x-cx)+(y-cy)*(y-cy) <= radius*radius {
|
|
||||||
if x >= bounds.Min.X && x < bounds.Max.X && y >= bounds.Min.Y && y < bounds.Max.Y {
|
|
||||||
img.SetNRGBA(x, y, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageToPNGBase64(img image.Image) (string, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := png.Encode(&buf, img); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
package captcha
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// VerifyInput holds the captcha fields from a login/register/reset request.
|
|
||||||
type VerifyInput struct {
|
|
||||||
CaptchaId string
|
|
||||||
CaptchaCode string
|
|
||||||
CfToken string
|
|
||||||
SliderToken string
|
|
||||||
IP string
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyCaptcha validates the captcha according to captchaType.
|
|
||||||
// Returns nil when captchaType is empty / unrecognised (i.e. captcha disabled).
|
|
||||||
func VerifyCaptcha(
|
|
||||||
ctx context.Context,
|
|
||||||
redisClient *redis.Client,
|
|
||||||
captchaType string,
|
|
||||||
turnstileSecret string,
|
|
||||||
input VerifyInput,
|
|
||||||
) error {
|
|
||||||
switch captchaType {
|
|
||||||
case string(CaptchaTypeLocal):
|
|
||||||
if input.CaptchaId == "" || input.CaptchaCode == "" {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
|
||||||
}
|
|
||||||
svc := NewService(Config{
|
|
||||||
Type: CaptchaTypeLocal,
|
|
||||||
RedisClient: redisClient,
|
|
||||||
})
|
|
||||||
valid, err := svc.Verify(ctx, input.CaptchaId, input.CaptchaCode, input.IP)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
|
||||||
}
|
|
||||||
|
|
||||||
case string(CaptchaTypeTurnstile):
|
|
||||||
if input.CfToken == "" {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required")
|
|
||||||
}
|
|
||||||
svc := NewService(Config{
|
|
||||||
Type: CaptchaTypeTurnstile,
|
|
||||||
TurnstileSecret: turnstileSecret,
|
|
||||||
})
|
|
||||||
valid, err := svc.Verify(ctx, input.CfToken, "", input.IP)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid captcha")
|
|
||||||
}
|
|
||||||
|
|
||||||
case string(CaptchaTypeSlider):
|
|
||||||
if input.SliderToken == "" {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "slider captcha required")
|
|
||||||
}
|
|
||||||
sliderSvc := NewSliderService(redisClient)
|
|
||||||
valid, err := sliderSvc.VerifySliderToken(ctx, input.SliderToken)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verify captcha error")
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "invalid slider captcha")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user