新增腾讯天御验证码认证门禁

This commit is contained in:
lyen1688
2026-08-04 15:09:29 +08:00
parent 825ca7b1fc
commit e592c5f9e0
86 changed files with 3683 additions and 262 deletions
@@ -156,6 +156,11 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
TurnstileEnabled: settings.TurnstileEnabled,
TurnstileSiteKey: settings.TurnstileSiteKey,
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
TencentCaptchaAppID: settings.TencentCaptchaAppID,
TencentCaptchaAppSecretKeyConfigured: settings.TencentCaptchaAppSecretKeyConfigured,
TencentCaptchaCloudSecretIDConfigured: settings.TencentCaptchaCloudSecretIDConfigured,
TencentCaptchaCloudSecretKeyConfigured: settings.TencentCaptchaCloudSecretKeyConfigured,
APIKeyACLTrustForwardedIP: settings.APIKeyACLTrustForwardedIP,
ForwardedClientIPHeaders: settings.ForwardedClientIPHeaders,
LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
@@ -107,6 +107,21 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if req.TurnstileSecretKey != "" {
changed = append(changed, "turnstile_secret_key")
}
if before.TencentCaptchaEnabled != after.TencentCaptchaEnabled {
changed = append(changed, "tencent_captcha_enabled")
}
if before.TencentCaptchaAppID != after.TencentCaptchaAppID {
changed = append(changed, "tencent_captcha_app_id")
}
if req.TencentCaptchaAppSecretKey != "" {
changed = append(changed, "tencent_captcha_app_secret_key")
}
if req.TencentCaptchaCloudSecretID != "" {
changed = append(changed, "tencent_captcha_cloud_secret_id")
}
if req.TencentCaptchaCloudSecretKey != "" {
changed = append(changed, "tencent_captcha_cloud_secret_key")
}
if before.APIKeyACLTrustForwardedIP != after.APIKeyACLTrustForwardedIP {
changed = append(changed, "api_key_acl_trust_forwarded_ip")
}
@@ -65,3 +65,75 @@ func TestUpdateSettingsSMTPFromAliasIsWritable(t *testing.T) {
require.Equal(t, "new@example.com", repo.values[service.SettingKeySMTPFrom])
}
func TestUpdateSettingsRejectsTwoCaptchaProviders(t *testing.T) {
h, _ := newStepUpSwitchTestHandler(t, map[string]string{
service.SettingKeyTurnstileEnabled: "true",
service.SettingKeyTurnstileSiteKey: "site-key",
service.SettingKeyTurnstileSecretKey: "turnstile-secret",
})
rec := doUpdateSettings(t, h, map[string]any{
"turnstile_enabled": true,
"turnstile_site_key": "site-key",
"turnstile_secret_key": "turnstile-secret",
"tencent_captcha_enabled": true,
"tencent_captcha_app_id": "123456789",
"tencent_captcha_app_secret_key": "app-secret",
"tencent_captcha_cloud_secret_id": "cloud-secret-id",
"tencent_captcha_cloud_secret_key": "cloud-secret-key",
}, nil)
require.Equal(t, http.StatusBadRequest, rec.Code)
require.Contains(t, rec.Body.String(), "cannot be enabled at the same time")
}
func TestUpdateSettingsRequiresFourTencentCaptchaCredentialsWhenEnabled(t *testing.T) {
h, _ := newStepUpSwitchTestHandler(t, map[string]string{})
rec := doUpdateSettings(t, h, map[string]any{
"tencent_captcha_enabled": true,
"tencent_captcha_app_id": "123456789",
}, nil)
require.Equal(t, http.StatusBadRequest, rec.Code)
require.Contains(t, rec.Body.String(), "AppSecretKey")
}
func TestUpdateSettingsRetainsStoredTencentCaptchaCredentialsWhenInputsEmpty(t *testing.T) {
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
service.SettingKeyTencentCaptchaAppSecretKey: "stored-app-secret",
service.SettingKeyTencentCaptchaCloudSecretID: "stored-cloud-secret-id",
service.SettingKeyTencentCaptchaCloudSecretKey: "stored-cloud-secret-key",
})
rec := doUpdateSettings(t, h, map[string]any{
"tencent_captcha_enabled": true,
"tencent_captcha_app_id": "123456789",
"tencent_captcha_app_secret_key": "",
"tencent_captcha_cloud_secret_id": "",
"tencent_captcha_cloud_secret_key": "",
}, nil)
require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, "stored-app-secret", repo.values[service.SettingKeyTencentCaptchaAppSecretKey])
require.Equal(t, "stored-cloud-secret-id", repo.values[service.SettingKeyTencentCaptchaCloudSecretID])
require.Equal(t, "stored-cloud-secret-key", repo.values[service.SettingKeyTencentCaptchaCloudSecretKey])
}
func TestUpdateSettingsValidatesTencentCaptchaAppIDWhenEnabledFlagIsOmitted(t *testing.T) {
h, _ := newStepUpSwitchTestHandler(t, map[string]string{
service.SettingKeyTencentCaptchaEnabled: "true",
service.SettingKeyTencentCaptchaAppID: "123456789",
service.SettingKeyTencentCaptchaAppSecretKey: "stored-app-secret",
service.SettingKeyTencentCaptchaCloudSecretID: "stored-cloud-secret-id",
service.SettingKeyTencentCaptchaCloudSecretKey: "stored-cloud-secret-key",
})
rec := doUpdateSettings(t, h, map[string]any{
"tencent_captcha_app_id": "not-a-number",
}, nil)
require.Equal(t, http.StatusBadRequest, rec.Code)
require.Contains(t, rec.Body.String(), "positive integer")
}
@@ -64,6 +64,23 @@ func TestDiffSettings_NoChangeWhenEqual(t *testing.T) {
}
}
func TestSettingsAuditRequestDoesNotInheritStoredTencentSecrets(t *testing.T) {
req := UpdateSettingsRequest{
TencentCaptchaAppSecretKey: " ",
TencentCaptchaCloudSecretID: "\t",
TencentCaptchaCloudSecretKey: "\n",
}
auditReq := settingsAuditRequest(req)
req.TencentCaptchaAppSecretKey = "stored-app-secret"
req.TencentCaptchaCloudSecretID = "stored-secret-id"
req.TencentCaptchaCloudSecretKey = "stored-secret-key"
require.Empty(t, auditReq.TencentCaptchaAppSecretKey)
require.Empty(t, auditReq.TencentCaptchaCloudSecretID)
require.Empty(t, auditReq.TencentCaptchaCloudSecretKey)
}
func TestDiffSettings_DetectsCompactHomeChange(t *testing.T) {
changed := diffSettings(
&service.SystemSettings{},
@@ -7,6 +7,7 @@ import (
"log/slog"
"net/http"
"reflect"
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/config"
@@ -53,6 +54,13 @@ type UpdateSettingsRequest struct {
TurnstileSiteKey string `json:"turnstile_site_key"`
TurnstileSecretKey string `json:"turnstile_secret_key"`
// 腾讯天御验证码设置
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
TencentCaptchaAppSecretKey string `json:"tencent_captcha_app_secret_key"`
TencentCaptchaCloudSecretID string `json:"tencent_captcha_cloud_secret_id"`
TencentCaptchaCloudSecretKey string `json:"tencent_captcha_cloud_secret_key"`
// API Key IP 访问控制设置
APIKeyACLTrustForwardedIP *bool `json:"api_key_acl_trust_forwarded_ip"`
ForwardedClientIPHeaders *[]string `json:"forwarded_client_ip_headers"`
@@ -436,6 +444,13 @@ func omittedSettingKeys(sentFields map[string]json.RawMessage) service.OmittedSe
return omitted
}
func settingsAuditRequest(req UpdateSettingsRequest) UpdateSettingsRequest {
req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
return req
}
func (h *SettingHandler) UpdateSettings(c *gin.Context) {
var sentFields map[string]json.RawMessage
if err := c.ShouldBindBodyWith(&sentFields, binding.JSON); err != nil {
@@ -447,6 +462,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
auditReq := settingsAuditRequest(req)
omitted := omittedSettingKeys(sentFields)
previousSettings, err := h.settingService.GetAllSettings(c.Request.Context())
@@ -563,6 +579,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
req.SMTPPassword = strings.TrimSpace(req.SMTPPassword)
req.SMTPFrom = strings.TrimSpace(req.SMTPFrom)
req.SMTPFromName = strings.TrimSpace(req.SMTPFromName)
req.TencentCaptchaAppID = strings.TrimSpace(req.TencentCaptchaAppID)
req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
if req.SMTPPort <= 0 {
req.SMTPPort = 587
}
@@ -584,6 +604,19 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
req.SMTPUseTLS = previousSettings.SMTPUseTLS
}
turnstileEnabled := req.TurnstileEnabled
if _, sent := sentFields["turnstile_enabled"]; !sent {
turnstileEnabled = previousSettings.TurnstileEnabled
}
tencentCaptchaEnabled := req.TencentCaptchaEnabled
if _, sent := sentFields["tencent_captcha_enabled"]; !sent {
tencentCaptchaEnabled = previousSettings.TencentCaptchaEnabled
}
if turnstileEnabled && tencentCaptchaEnabled {
response.BadRequest(c, "Cloudflare Turnstile and Tencent Captcha cannot be enabled at the same time")
return
}
// Turnstile 参数验证
if req.TurnstileEnabled {
// 检查必填字段
@@ -611,6 +644,38 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
}
if tencentCaptchaEnabled {
if _, sent := sentFields["tencent_captcha_app_id"]; !sent {
req.TencentCaptchaAppID = previousSettings.TencentCaptchaAppID
}
appID, err := strconv.ParseUint(req.TencentCaptchaAppID, 10, 64)
if err != nil || appID == 0 {
response.BadRequest(c, "Tencent Captcha CaptchaAppId must be a positive integer when enabled")
return
}
if req.TencentCaptchaAppSecretKey == "" {
req.TencentCaptchaAppSecretKey = previousSettings.TencentCaptchaAppSecretKey
}
if req.TencentCaptchaCloudSecretID == "" {
req.TencentCaptchaCloudSecretID = previousSettings.TencentCaptchaCloudSecretID
}
if req.TencentCaptchaCloudSecretKey == "" {
req.TencentCaptchaCloudSecretKey = previousSettings.TencentCaptchaCloudSecretKey
}
if req.TencentCaptchaAppSecretKey == "" {
response.BadRequest(c, "Tencent Captcha AppSecretKey is required when enabled")
return
}
if req.TencentCaptchaCloudSecretID == "" {
response.BadRequest(c, "Tencent Cloud SecretId is required when Tencent Captcha is enabled")
return
}
if req.TencentCaptchaCloudSecretKey == "" {
response.BadRequest(c, "Tencent Cloud SecretKey is required when Tencent Captcha is enabled")
return
}
}
// TOTP 双因素认证参数验证
// 只有手动配置了加密密钥才允许启用 TOTP 功能
if req.TotpEnabled && !previousSettings.TotpEnabled {
@@ -1349,6 +1414,11 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
TurnstileEnabled: req.TurnstileEnabled,
TurnstileSiteKey: req.TurnstileSiteKey,
TurnstileSecretKey: req.TurnstileSecretKey,
TencentCaptchaEnabled: req.TencentCaptchaEnabled,
TencentCaptchaAppID: req.TencentCaptchaAppID,
TencentCaptchaAppSecretKey: req.TencentCaptchaAppSecretKey,
TencentCaptchaCloudSecretID: req.TencentCaptchaCloudSecretID,
TencentCaptchaCloudSecretKey: req.TencentCaptchaCloudSecretKey,
APIKeyACLTrustForwardedIP: func() bool {
if req.APIKeyACLTrustForwardedIP != nil {
return *req.APIKeyACLTrustForwardedIP
@@ -1844,7 +1914,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
}
h.auditSettingsUpdate(c, previousSettings, settings, previousAuthSourceDefaults, authSourceDefaults, req)
h.auditSettingsUpdate(c, previousSettings, settings, previousAuthSourceDefaults, authSourceDefaults, auditReq)
// 重新获取设置返回
updatedSettings, err := h.settingService.GetAllSettings(c.Request.Context())
@@ -1907,6 +1977,11 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
TurnstileEnabled: updatedSettings.TurnstileEnabled,
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
TencentCaptchaEnabled: updatedSettings.TencentCaptchaEnabled,
TencentCaptchaAppID: updatedSettings.TencentCaptchaAppID,
TencentCaptchaAppSecretKeyConfigured: updatedSettings.TencentCaptchaAppSecretKeyConfigured,
TencentCaptchaCloudSecretIDConfigured: updatedSettings.TencentCaptchaCloudSecretIDConfigured,
TencentCaptchaCloudSecretKeyConfigured: updatedSettings.TencentCaptchaCloudSecretKeyConfigured,
APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
ForwardedClientIPHeaders: updatedSettings.ForwardedClientIPHeaders,
LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,