Add Cap Captcha support (#2511)

* Add Cap Captcha support

- Add CaptchaCap type constant in types.go
- Add Cap struct with InstanceURL, KeyID, and KeySecret fields
- Add CapCaptcha method in provider.go to return Cap settings
- Add default settings for Cap captcha in setting.go
- Implement Cap captcha verification logic in middleware
- Expose Cap captcha settings in site API

This adds support for Cap captcha service as an alternative
captcha option alongside existing reCAPTCHA, Turnstile and
built-in captcha options.

* update cap json tags
pull/2557/head
WittF 2025-06-19 11:31:17 +08:00 committed by GitHub
parent 9a216cd09e
commit 9f9796f2f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 83 additions and 0 deletions

View File

@ -143,6 +143,9 @@ var DefaultSettings = map[string]string{
"captcha_ReCaptchaSecret": "defaultSecret",
"captcha_turnstile_site_key": "",
"captcha_turnstile_site_secret": "",
"captcha_cap_instance_url": "",
"captcha_cap_key_id": "",
"captcha_cap_key_secret": "",
"thumb_width": "400",
"thumb_height": "300",
"thumb_entity_suffix": "._thumb",

View File

@ -38,6 +38,9 @@ type (
turnstileResponse struct {
Success bool `json:"success"`
}
capResponse struct {
Success bool `json:"success"`
}
)
// CaptchaRequired 验证请求签名
@ -127,6 +130,61 @@ func CaptchaRequired(enabled func(c *gin.Context) bool) gin.HandlerFunc {
return
}
break
case setting.CaptchaCap:
captchaSetting := settings.CapCaptcha(c)
if captchaSetting.InstanceURL == "" || captchaSetting.KeyID == "" || captchaSetting.KeySecret == "" {
l.Warning("Cap verification failed: missing configuration")
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha configuration error", nil))
c.Abort()
return
}
r := dep.RequestClient(
request2.WithContext(c),
request2.WithLogger(logging.FromContext(c)),
request2.WithHeader(http.Header{"Content-Type": []string{"application/json"}}),
)
capEndpoint := strings.TrimSuffix(captchaSetting.InstanceURL, "/") + "/" + captchaSetting.KeyID + "/siteverify"
requestBody := map[string]string{
"secret": captchaSetting.KeySecret,
"response": service.Ticket,
}
requestData, err := json.Marshal(requestBody)
if err != nil {
l.Warning("Cap verification failed: %s", err)
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
c.Abort()
return
}
res, err := r.Request("POST", capEndpoint, strings.NewReader(string(requestData))).
CheckHTTPResponse(http.StatusOK).
GetResponse()
if err != nil {
l.Warning("Cap verification failed: %s", err)
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
c.Abort()
return
}
var capRes capResponse
err = json.Unmarshal([]byte(res), &capRes)
if err != nil {
l.Warning("Cap verification failed: %s", err)
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", err))
c.Abort()
return
}
if !capRes.Success {
l.Warning("Cap verification failed: validation returned false")
c.JSON(200, serializer.ErrWithDetails(c, serializer.CodeCaptchaError, "Captcha validation failed", nil))
c.Abort()
return
}
break
}
}

View File

@ -38,6 +38,8 @@ type (
TcCaptcha(ctx context.Context) *TcCaptcha
// TurnstileCaptcha returns the Cloudflare Turnstile settings.
TurnstileCaptcha(ctx context.Context) *Turnstile
// CapCaptcha returns the Cap settings.
CapCaptcha(ctx context.Context) *Cap
// EmailActivationEnabled returns true if email activation is required.
EmailActivationEnabled(ctx context.Context) bool
// DefaultGroup returns the default group ID for new users.
@ -638,6 +640,14 @@ func (s *settingProvider) TurnstileCaptcha(ctx context.Context) *Turnstile {
}
}
func (s *settingProvider) CapCaptcha(ctx context.Context) *Cap {
return &Cap{
InstanceURL: s.getString(ctx, "captcha_cap_instance_url", ""),
KeyID: s.getString(ctx, "captcha_cap_key_id", ""),
KeySecret: s.getString(ctx, "captcha_cap_key_secret", ""),
}
}
func (s *settingProvider) ReCaptcha(ctx context.Context) *ReCaptcha {
return &ReCaptcha{
Secret: s.getString(ctx, "captcha_ReCaptchaSecret", ""),

View File

@ -28,6 +28,7 @@ const (
CaptchaReCaptcha = CaptchaType("recaptcha")
CaptchaTcaptcha = CaptchaType("tcaptcha")
CaptchaTurnstile = CaptchaType("turnstile")
CaptchaCap = CaptchaType("cap")
)
type ReCaptcha struct {
@ -47,6 +48,12 @@ type Turnstile struct {
Secret string
}
type Cap struct {
InstanceURL string
KeyID string
KeySecret string
}
type SMTP struct {
FromName string
From string

View File

@ -28,6 +28,8 @@ type SiteConfig struct {
ReCaptchaKey string `json:"captcha_ReCaptchaKey,omitempty"`
CaptchaType setting.CaptchaType `json:"captcha_type,omitempty"`
TurnstileSiteID string `json:"turnstile_site_id,omitempty"`
CapInstanceURL string `json:"captcha_cap_instance_url,omitempty"`
CapKeyID string `json:"captcha_cap_key_id,omitempty"`
RegisterEnabled bool `json:"register_enabled,omitempty"`
TosUrl string `json:"tos_url,omitempty"`
PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"`
@ -119,6 +121,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
userRes := user.BuildUser(u, dep.HashIDEncoder())
logo := settings.Logo(c)
reCaptcha := settings.ReCaptcha(c)
capCaptcha := settings.CapCaptcha(c)
appSetting := settings.AppSetting(c)
return &SiteConfig{
@ -132,6 +135,8 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
CaptchaType: settings.CaptchaType(c),
TurnstileSiteID: settings.TurnstileCaptcha(c).Key,
ReCaptchaKey: reCaptcha.Key,
CapInstanceURL: capCaptcha.InstanceURL,
CapKeyID: capCaptcha.KeyID,
AppPromotion: appSetting.Promotion,
}, nil
}