From 9aaffec61dd064e5f9c35560e8ed494f0b501de7 Mon Sep 17 00:00:00 2001 From: EUForest Date: Mon, 9 Mar 2026 22:54:59 +0800 Subject: [PATCH] feat(auth): add admin authentication with permission checks - Add admin login handler and logic with IsAdmin verification - Add admin password reset handler and logic - Add admin captcha generation handler and logic - Implement device binding for admin login - Add login logging for admin authentication - Check EnableAdminLoginCaptcha configuration - Separate admin authentication from user authentication - Verify admin permission before allowing access --- .../auth/admin/adminGenerateCaptchaHandler.go | 18 ++ .../handler/auth/admin/adminLoginHandler.go | 30 +++ .../auth/admin/adminResetPasswordHandler.go | 29 +++ .../auth/admin/adminGenerateCaptchaLogic.go | 70 ++++++ internal/logic/auth/admin/adminLoginLogic.go | 201 +++++++++++++++ .../auth/admin/adminResetPasswordLogic.go | 229 ++++++++++++++++++ 6 files changed, 577 insertions(+) create mode 100644 internal/handler/auth/admin/adminGenerateCaptchaHandler.go create mode 100644 internal/handler/auth/admin/adminLoginHandler.go create mode 100644 internal/handler/auth/admin/adminResetPasswordHandler.go create mode 100644 internal/logic/auth/admin/adminGenerateCaptchaLogic.go create mode 100644 internal/logic/auth/admin/adminLoginLogic.go create mode 100644 internal/logic/auth/admin/adminResetPasswordLogic.go diff --git a/internal/handler/auth/admin/adminGenerateCaptchaHandler.go b/internal/handler/auth/admin/adminGenerateCaptchaHandler.go new file mode 100644 index 0000000..caabd45 --- /dev/null +++ b/internal/handler/auth/admin/adminGenerateCaptchaHandler.go @@ -0,0 +1,18 @@ +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/pkg/result" +) + +// Generate captcha +func AdminGenerateCaptchaHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := admin.NewAdminGenerateCaptchaLogic(c.Request.Context(), svcCtx) + resp, err := l.AdminGenerateCaptcha() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/admin/adminLoginHandler.go b/internal/handler/auth/admin/adminLoginHandler.go new file mode 100644 index 0000000..95239bd --- /dev/null +++ b/internal/handler/auth/admin/adminLoginHandler.go @@ -0,0 +1,30 @@ +package admin + +import ( + "github.com/gin-gonic/gin" + adminLogic "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" +) + +// Admin login +func AdminLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UserLoginRequest + _ = c.ShouldBind(&req) + // get client ip + req.IP = c.ClientIP() + req.UserAgent = c.Request.UserAgent() + + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := adminLogic.NewAdminLoginLogic(c.Request.Context(), svcCtx) + resp, err := l.AdminLogin(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/admin/adminResetPasswordHandler.go b/internal/handler/auth/admin/adminResetPasswordHandler.go new file mode 100644 index 0000000..9fb909c --- /dev/null +++ b/internal/handler/auth/admin/adminResetPasswordHandler.go @@ -0,0 +1,29 @@ +package admin + +import ( + "github.com/gin-gonic/gin" + adminLogic "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" +) + +// Admin reset password +func AdminResetPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetPasswordRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + // get client ip + req.IP = c.ClientIP() + req.UserAgent = c.Request.UserAgent() + + l := adminLogic.NewAdminResetPasswordLogic(c.Request.Context(), svcCtx) + resp, err := l.AdminResetPassword(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/logic/auth/admin/adminGenerateCaptchaLogic.go b/internal/logic/auth/admin/adminGenerateCaptchaLogic.go new file mode 100644 index 0000000..4772855 --- /dev/null +++ b/internal/logic/auth/admin/adminGenerateCaptchaLogic.go @@ -0,0 +1,70 @@ +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 AdminGenerateCaptchaLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Generate captcha +func NewAdminGenerateCaptchaLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGenerateCaptchaLogic { + return &AdminGenerateCaptchaLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGenerateCaptchaLogic) AdminGenerateCaptcha() (resp *types.GenerateCaptchaResponse, err error) { + resp = &types.GenerateCaptchaResponse{} + + // Get verify config from database + verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx) + if err != nil { + l.Logger.Error("[AdminGenerateCaptchaLogic] 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"` + TurnstileSiteKey string `json:"turnstile_site_key"` + TurnstileSecret string `json:"turnstile_secret"` + } + tool.SystemConfigSliceReflectToStruct(verifyCfg, &config) + + resp.Type = config.CaptchaType + + // If captcha type is local, generate captcha image + if config.CaptchaType == "local" { + captchaService := captcha.NewService(captcha.Config{ + Type: captcha.CaptchaTypeLocal, + RedisClient: l.svcCtx.Redis, + }) + + id, image, err := captchaService.Generate(l.ctx) + if err != nil { + l.Logger.Error("[AdminGenerateCaptchaLogic] Generate captcha error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Generate captcha error: %v", err.Error()) + } + + resp.Id = id + resp.Image = image + } else if config.CaptchaType == "turnstile" { + // For Turnstile, just return the site key + resp.Id = config.TurnstileSiteKey + } + + return resp, nil +} diff --git a/internal/logic/auth/admin/adminLoginLogic.go b/internal/logic/auth/admin/adminLoginLogic.go new file mode 100644 index 0000000..2167eaf --- /dev/null +++ b/internal/logic/auth/admin/adminLoginLogic.go @@ -0,0 +1,201 @@ +package admin + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "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/constant" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type AdminLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Admin login +func NewAdminLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminLoginLogic { + return &AdminLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminLoginLogic) AdminLogin(req *types.UserLoginRequest) (resp *types.LoginResponse, err error) { + loginStatus := false + var userInfo *user.User + // Record login status + defer func(svcCtx *svc.ServiceContext) { + if userInfo != nil && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "email", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }(l.svcCtx) + + // Verify captcha + if err := l.verifyCaptcha(req); err != nil { + return nil, err + } + + userInfo, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) + + if userInfo.DeletedAt.Valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email deleted: %v", req.Email) + } + + if err != nil { + if errors.As(err, &gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) + } + logger.WithContext(l.ctx).Error(err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + + // Check if user is admin + if userInfo.IsAdmin == nil || !*userInfo.IsAdmin { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.PermissionDenied), "user is not admin") + } + + // Verify password + if !tool.MultiPasswordVerify(userInfo.Algo, userInfo.Salt, req.Password, userInfo.Password) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") + } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := auth.NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + if l.ctx.Value(constant.CtxLoginType) != nil { + req.LoginType = l.ctx.Value(constant.CtxLoginType).(string) + } + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + jwt.WithOption("identifier", req.Identifier), + jwt.WithOption("CtxLoginType", req.LoginType), + ) + if err != nil { + l.Logger.Error("[AdminLogin] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *AdminLoginLogic) verifyCaptcha(req *types.UserLoginRequest) error { + // Get verify config from database + verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx) + if err != nil { + l.Logger.Error("[AdminLoginLogic] GetVerifyConfig error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error()) + } + + var config struct { + CaptchaType string `json:"captcha_type"` + EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"` + TurnstileSecret string `json:"turnstile_secret"` + } + tool.SystemConfigSliceReflectToStruct(verifyCfg, &config) + + // Check if captcha is enabled for admin login + if !config.EnableAdminLoginCaptcha { + return nil + } + + // Verify based on captcha type + if config.CaptchaType == "local" { + if req.CaptchaId == "" || req.CaptchaCode == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required") + } + + 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 +} diff --git a/internal/logic/auth/admin/adminResetPasswordLogic.go b/internal/logic/auth/admin/adminResetPasswordLogic.go new file mode 100644 index 0000000..544b56b --- /dev/null +++ b/internal/logic/auth/admin/adminResetPasswordLogic.go @@ -0,0 +1,229 @@ +package admin + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "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/constant" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type AdminResetPasswordLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +type CacheKeyPayload struct { + Code string `json:"code"` +} + +// Admin reset password +func NewAdminResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminResetPasswordLogic { + return &AdminResetPasswordLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminResetPasswordLogic) AdminResetPassword(req *types.ResetPasswordRequest) (resp *types.LoginResponse, err error) { + var userInfo *user.User + loginStatus := false + + defer func() { + if userInfo != nil && userInfo.Id != 0 && loginStatus { + loginLog := log.Login{ + Method: "email", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }() + + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) + // Check the verification code + if value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result(); err != nil { + l.Errorw("Verification code error", logger.Field("cacheKey", cacheKey), logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error") + } else { + var payload CacheKeyPayload + if err := json.Unmarshal([]byte(value), &payload); err != nil { + l.Errorw("Unmarshal errors", logger.Field("cacheKey", cacheKey), logger.Field("error", err.Error()), logger.Field("value", value)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error") + } + if payload.Code != req.Code { + l.Errorw("Verification code error", logger.Field("cacheKey", cacheKey), logger.Field("error", "Verification code error"), logger.Field("reqCode", req.Code), logger.Field("payloadCode", payload.Code)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error") + } + } + + // Verify captcha + if err := l.verifyCaptcha(req); err != nil { + return nil, err + } + + // Check user + authMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user by email error: %v", err.Error()) + } + + userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethod.UserId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) + } + + // Check if user is admin + if userInfo.IsAdmin == nil || !*userInfo.IsAdmin { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.PermissionDenied), "user is not admin") + } + + // Update password + userInfo.Password = tool.EncodePassWord(req.Password) + userInfo.Algo = "default" + if err = l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) + } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := auth.NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.CtxLoginType) != nil { + req.LoginType = l.ctx.Value(constant.CtxLoginType).(string) + } + // Generate session id + sessionId := uuidx.NewUUID().String() + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + jwt.WithOption("identifier", req.Identifier), + jwt.WithOption("CtxLoginType", req.LoginType), + ) + if err != nil { + l.Logger.Error("[AdminResetPassword] token generate error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *AdminResetPasswordLogic) verifyCaptcha(req *types.ResetPasswordRequest) error { + // Get verify config from database + verifyCfg, err := l.svcCtx.SystemModel.GetVerifyConfig(l.ctx) + if err != nil { + l.Logger.Error("[AdminResetPasswordLogic] GetVerifyConfig error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetVerifyConfig error: %v", err.Error()) + } + + var config struct { + CaptchaType string `json:"captcha_type"` + EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"` + TurnstileSecret string `json:"turnstile_secret"` + } + tool.SystemConfigSliceReflectToStruct(verifyCfg, &config) + + // Check if admin login captcha is enabled (use admin login captcha for reset password) + if !config.EnableAdminLoginCaptcha { + return nil + } + + // Verify based on captcha type + if config.CaptchaType == "local" { + if req.CaptchaId == "" || req.CaptchaCode == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "captcha required") + } + + 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 +}