mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:18:18 +08:00
人机验证增加阿里云验证码 2.0
沿用腾讯天御验证码引入的多服务商模型:aliyun_captcha_enabled 作为独立 开关,与 Cloudflare Turnstile、腾讯天御三方互斥(保存校验 + 运行时 CAPTCHA_PROVIDER_CONFLICT)。后台「安全与认证」合并为单张人机验证卡片: 总开关 + 服务商单选(Turnstile / 腾讯天御 / 阿里云),选中即启用该家并 关闭其它,落库仍是三个独立开关键,由前端映射保证互斥。 阿里云侧同时支持 aliyun 中国站与国际站(alibabacloud.com):两站前端脚本、 region 取值与服务端 API 完全一致,仅账号与 AccessKey 相互独立,因此由 「服务地域」决定线路即可——中国内地走 captcha.cn-shanghai.aliyuncs.com, 非中国内地(新加坡)走 captcha.ap-southeast-1.aliyuncs.com,AccessKey 取自持有该实例的账号,无需在配置中区分站点。 - AliyunCaptchaService 对称 TencentCaptchaService:服务端校验走官方 SDK VerifyIntelligentCaptcha,调用异常按 fail-closed 拦截,与 Turnstile 网络错误行为对称;保存设置时真实探测 AK/SK 有效性 - 保护面对齐腾讯扩展入口:VerifyTencentCaptchaIfEnabled 通用化为 VerifyActionCaptchaIfEnabled,OAuth 登录启动、passkey 登录在阿里云 启用时同样拦截;Turnstile 维持既有覆盖不扩大 - 前端 AliyunCaptchaWidget 为表单内预验证按钮(popup 模式),同时暴露 verify() 供 OAuth 启动、passkey 等动作入口程序化弹窗;未预验证直接 提交时弹窗兜底。SDK 按钮绑定异步完成,弹窗未出现前按 tick 重试触发, 并轮询弹窗可见性识别用户关闭 - captchaVerifyParam 复用 turnstile_token 请求字段提交;公开设置下发 aliyun_captcha_enabled / scene_id / prefix / region - CSP 放行验证码 CDN:script-src/style-src 加 *.alicdn.com
This commit is contained in:
@@ -59,6 +59,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
turnstileService := service.NewTurnstileService(settingService, turnstileVerifier)
|
||||
tencentCaptchaVerifier := repository.NewTencentCaptchaVerifier()
|
||||
tencentCaptchaService := service.NewTencentCaptchaService(settingService, tencentCaptchaVerifier)
|
||||
aliyunCaptchaVerifier := repository.NewAliyunCaptchaVerifier()
|
||||
aliyunCaptchaService := service.NewAliyunCaptchaService(settingService, aliyunCaptchaVerifier)
|
||||
emailQueueService := service.ProvideEmailQueueService(emailService)
|
||||
promoCodeRepository := repository.NewPromoCodeRepository(client)
|
||||
billingCache := repository.NewBillingCache(redisClient)
|
||||
@@ -80,7 +82,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
subscriptionService := service.NewSubscriptionService(groupRepository, userSubscriptionRepository, billingCacheService, client, configConfig)
|
||||
affiliateRepository := repository.NewAffiliateRepository(client, db)
|
||||
affiliateService := service.NewAffiliateService(affiliateRepository, settingService, apiKeyAuthCacheInvalidator, billingCacheService)
|
||||
authService := service.ProvideAuthService(client, userRepository, redeemCodeRepository, refreshTokenCache, configConfig, settingService, emailService, turnstileService, tencentCaptchaService, emailQueueService, promoService, subscriptionService, affiliateService, serviceUserPlatformQuotaRepository)
|
||||
authService := service.ProvideAuthService(client, userRepository, redeemCodeRepository, refreshTokenCache, configConfig, settingService, emailService, turnstileService, tencentCaptchaService, aliyunCaptchaService, emailQueueService, promoService, subscriptionService, affiliateService, serviceUserPlatformQuotaRepository)
|
||||
userService := service.NewUserService(userRepository, settingRepository, apiKeyAuthCacheInvalidator, billingCache)
|
||||
redeemCache := repository.NewRedeemCache(redisClient)
|
||||
redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client, apiKeyAuthCacheInvalidator, affiliateService)
|
||||
@@ -224,7 +226,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
registry := payment.ProvideRegistry()
|
||||
defaultLoadBalancer := payment.ProvideDefaultLoadBalancer(client, encryptionKey)
|
||||
paymentService := service.ProvidePaymentService(client, registry, defaultLoadBalancer, redeemService, subscriptionService, paymentConfigService, userRepository, groupRepository, affiliateService, notificationEmailService)
|
||||
settingHandler := handler.ProvideAdminSettingHandler(settingService, emailService, turnstileService, opsService, paymentConfigService, paymentService, userAttributeService, notificationEmailService, totpService, userService)
|
||||
settingHandler := handler.ProvideAdminSettingHandler(settingService, emailService, turnstileService, aliyunCaptchaService, opsService, paymentConfigService, paymentService, userAttributeService, notificationEmailService, totpService, userService)
|
||||
opsHandler := admin.NewOpsHandler(opsService)
|
||||
updateCache := repository.NewUpdateCache(redisClient)
|
||||
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
||||
|
||||
+11
-2
@@ -5,6 +5,9 @@ go 1.26.5
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
|
||||
github.com/alibabacloud-go/tea v1.3.13
|
||||
github.com/alicebob/miniredis/v2 v2.38.0
|
||||
github.com/alitto/pond/v2 v2.6.2
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
@@ -35,6 +38,8 @@ require (
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stripe/stripe-go/v85 v85.0.0
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.3.52
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.52
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
|
||||
github.com/testcontainers/testcontainers-go/modules/redis v0.40.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
@@ -60,6 +65,10 @@ require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.5 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
@@ -81,6 +90,7 @@ require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
@@ -161,12 +171,11 @@ require (
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/captcha v1.3.52 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.52 // indirect
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
||||
+225
-12
@@ -1,5 +1,6 @@
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc=
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
@@ -8,6 +9,7 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -16,10 +18,56 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
|
||||
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3 h1:0Aobw12m3x28aeDMPjwjXsfF8MuLvRjlQ4Hhoy5hFOY=
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3/go.mod h1:ydzBIN2OiM7eeQPpAFyBrv1H5TY1MtUP2rQig44C4UQ=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
|
||||
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/alitto/pond/v2 v2.6.2 h1:Sphe40g0ILeM1pA2c2K+Th0DGU+pt0A/Kprr+WB24Pw=
|
||||
github.com/alitto/pond/v2 v2.6.2/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
|
||||
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
@@ -77,11 +125,16 @@ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -120,6 +173,9 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -163,27 +219,43 @@ github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
||||
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
@@ -207,8 +279,10 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
@@ -217,6 +291,8 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
@@ -234,8 +310,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
||||
@@ -263,14 +337,15 @@ github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -292,6 +367,7 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
@@ -304,8 +380,6 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -330,6 +404,9 @@ github.com/smartwalle/ngx v1.1.0 h1:q8nANgWSPRGeI/u+ixBoA4mf68DrUq6vZ+n9L5UKv9I=
|
||||
github.com/smartwalle/ngx v1.1.0/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
|
||||
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
|
||||
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
@@ -338,18 +415,18 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -386,6 +463,9 @@ github.com/tiktoken-go/tokenizer v0.8.0 h1:drHWno2Zx3eAm/hk/LmvBKXPpSImB7BRyh/ru
|
||||
github.com/tiktoken-go/tokenizer v0.8.0/go.mod h1:pTmPz4r14MV3JkUGAmAcdLdYhSxN68MCjrP+EoxBdx0=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
@@ -400,6 +480,9 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
@@ -443,59 +526,189 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
|
||||
@@ -32,7 +32,7 @@ const (
|
||||
|
||||
// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
|
||||
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
|
||||
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
|
||||
// UMQ(用户消息队列)模式常量
|
||||
const (
|
||||
|
||||
@@ -54,6 +54,7 @@ type SettingHandler struct {
|
||||
settingService *service.SettingService
|
||||
emailService *service.EmailService
|
||||
turnstileService *service.TurnstileService
|
||||
aliyunCaptchaService *service.AliyunCaptchaService
|
||||
opsService *service.OpsService
|
||||
paymentConfigService *service.PaymentConfigService
|
||||
paymentService *service.PaymentService
|
||||
@@ -82,6 +83,12 @@ func (h *SettingHandler) SetNotificationEmailService(notificationEmailService *s
|
||||
h.notificationEmailService = notificationEmailService
|
||||
}
|
||||
|
||||
// SetAliyunCaptchaService attaches the Aliyun captcha credential validator without
|
||||
// changing the constructor signature used by existing unit tests.
|
||||
func (h *SettingHandler) SetAliyunCaptchaService(aliyunCaptchaService *service.AliyunCaptchaService) {
|
||||
h.aliyunCaptchaService = aliyunCaptchaService
|
||||
}
|
||||
|
||||
// SetStepUpDeps attaches the services backing the step-up switch preconditions
|
||||
// (enable requires the acting admin to have TOTP enabled; disable is itself a
|
||||
// step-up gated operation), without changing the constructor signature used by
|
||||
@@ -161,6 +168,12 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
TencentCaptchaAppSecretKeyConfigured: settings.TencentCaptchaAppSecretKeyConfigured,
|
||||
TencentCaptchaCloudSecretIDConfigured: settings.TencentCaptchaCloudSecretIDConfigured,
|
||||
TencentCaptchaCloudSecretKeyConfigured: settings.TencentCaptchaCloudSecretKeyConfigured,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: settings.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecretConfigured: settings.AliyunCaptchaAccessKeySecretConfigured,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
APIKeyACLTrustForwardedIP: settings.APIKeyACLTrustForwardedIP,
|
||||
ForwardedClientIPHeaders: settings.ForwardedClientIPHeaders,
|
||||
LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
|
||||
|
||||
@@ -122,6 +122,24 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if req.TencentCaptchaCloudSecretKey != "" {
|
||||
changed = append(changed, "tencent_captcha_cloud_secret_key")
|
||||
}
|
||||
if before.AliyunCaptchaEnabled != after.AliyunCaptchaEnabled {
|
||||
changed = append(changed, "aliyun_captcha_enabled")
|
||||
}
|
||||
if before.AliyunCaptchaAccessKeyID != after.AliyunCaptchaAccessKeyID {
|
||||
changed = append(changed, "aliyun_captcha_access_key_id")
|
||||
}
|
||||
if req.AliyunCaptchaAccessKeySecret != "" {
|
||||
changed = append(changed, "aliyun_captcha_access_key_secret")
|
||||
}
|
||||
if before.AliyunCaptchaSceneID != after.AliyunCaptchaSceneID {
|
||||
changed = append(changed, "aliyun_captcha_scene_id")
|
||||
}
|
||||
if before.AliyunCaptchaPrefix != after.AliyunCaptchaPrefix {
|
||||
changed = append(changed, "aliyun_captcha_prefix")
|
||||
}
|
||||
if before.AliyunCaptchaRegion != after.AliyunCaptchaRegion {
|
||||
changed = append(changed, "aliyun_captcha_region")
|
||||
}
|
||||
if before.APIKeyACLTrustForwardedIP != after.APIKeyACLTrustForwardedIP {
|
||||
changed = append(changed, "api_key_acl_trust_forwarded_ip")
|
||||
}
|
||||
|
||||
@@ -61,6 +61,14 @@ type UpdateSettingsRequest struct {
|
||||
TencentCaptchaCloudSecretID string `json:"tencent_captcha_cloud_secret_id"`
|
||||
TencentCaptchaCloudSecretKey string `json:"tencent_captcha_cloud_secret_key"`
|
||||
|
||||
// 阿里云验证码 2.0 设置
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaAccessKeyID string `json:"aliyun_captcha_access_key_id"`
|
||||
AliyunCaptchaAccessKeySecret string `json:"aliyun_captcha_access_key_secret"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
|
||||
// API Key IP 访问控制设置
|
||||
APIKeyACLTrustForwardedIP *bool `json:"api_key_acl_trust_forwarded_ip"`
|
||||
ForwardedClientIPHeaders *[]string `json:"forwarded_client_ip_headers"`
|
||||
@@ -450,6 +458,7 @@ func settingsAuditRequest(req UpdateSettingsRequest) UpdateSettingsRequest {
|
||||
req.TencentCaptchaAppSecretKey = strings.TrimSpace(req.TencentCaptchaAppSecretKey)
|
||||
req.TencentCaptchaCloudSecretID = strings.TrimSpace(req.TencentCaptchaCloudSecretID)
|
||||
req.TencentCaptchaCloudSecretKey = strings.TrimSpace(req.TencentCaptchaCloudSecretKey)
|
||||
req.AliyunCaptchaAccessKeySecret = strings.TrimSpace(req.AliyunCaptchaAccessKeySecret)
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -614,10 +623,27 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
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")
|
||||
aliyunCaptchaEnabled := req.AliyunCaptchaEnabled
|
||||
if _, sent := sentFields["aliyun_captcha_enabled"]; !sent {
|
||||
aliyunCaptchaEnabled = previousSettings.AliyunCaptchaEnabled
|
||||
}
|
||||
enabledCaptchaProviders := 0
|
||||
for _, enabled := range []bool{turnstileEnabled, tencentCaptchaEnabled, aliyunCaptchaEnabled} {
|
||||
if enabled {
|
||||
enabledCaptchaProviders++
|
||||
}
|
||||
}
|
||||
if enabledCaptchaProviders > 1 {
|
||||
response.BadRequest(c, "Multiple captcha providers (Cloudflare Turnstile / Tencent Captcha / Aliyun Captcha) cannot be enabled at the same time")
|
||||
return
|
||||
}
|
||||
// 阿里云地域 normalize:未发送保留已存值,非法值一律按中国内地落库
|
||||
if _, sent := sentFields["aliyun_captcha_region"]; !sent {
|
||||
req.AliyunCaptchaRegion = previousSettings.AliyunCaptchaRegion
|
||||
}
|
||||
if req.AliyunCaptchaRegion != service.AliyunCaptchaRegionSGP {
|
||||
req.AliyunCaptchaRegion = service.AliyunCaptchaRegionCN
|
||||
}
|
||||
|
||||
// Turnstile 参数验证
|
||||
if req.TurnstileEnabled {
|
||||
@@ -678,6 +704,51 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 阿里云验证码 2.0 参数验证
|
||||
if aliyunCaptchaEnabled {
|
||||
if _, sent := sentFields["aliyun_captcha_scene_id"]; !sent {
|
||||
req.AliyunCaptchaSceneID = previousSettings.AliyunCaptchaSceneID
|
||||
}
|
||||
if _, sent := sentFields["aliyun_captcha_prefix"]; !sent {
|
||||
req.AliyunCaptchaPrefix = previousSettings.AliyunCaptchaPrefix
|
||||
}
|
||||
if _, sent := sentFields["aliyun_captcha_access_key_id"]; !sent {
|
||||
req.AliyunCaptchaAccessKeyID = previousSettings.AliyunCaptchaAccessKeyID
|
||||
}
|
||||
if req.AliyunCaptchaSceneID == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha Scene ID is required when enabled")
|
||||
return
|
||||
}
|
||||
if req.AliyunCaptchaPrefix == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha Prefix is required when enabled")
|
||||
return
|
||||
}
|
||||
if req.AliyunCaptchaAccessKeyID == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha AccessKey ID is required when enabled")
|
||||
return
|
||||
}
|
||||
// 如果未提供 AccessKey Secret,使用已保存的值(留空保留当前值)
|
||||
if req.AliyunCaptchaAccessKeySecret == "" {
|
||||
if previousSettings.AliyunCaptchaAccessKeySecret == "" {
|
||||
response.BadRequest(c, "Aliyun Captcha AccessKey Secret is required when enabled")
|
||||
return
|
||||
}
|
||||
req.AliyunCaptchaAccessKeySecret = previousSettings.AliyunCaptchaAccessKeySecret
|
||||
}
|
||||
|
||||
// 凭证任一变化时真实调用一次阿里云校验(避免配置错误导致无法登录)
|
||||
credentialsChanged := previousSettings.AliyunCaptchaAccessKeyID != req.AliyunCaptchaAccessKeyID ||
|
||||
previousSettings.AliyunCaptchaAccessKeySecret != req.AliyunCaptchaAccessKeySecret ||
|
||||
previousSettings.AliyunCaptchaSceneID != req.AliyunCaptchaSceneID ||
|
||||
previousSettings.AliyunCaptchaRegion != req.AliyunCaptchaRegion
|
||||
if credentialsChanged {
|
||||
if err := h.aliyunCaptchaService.ValidateCredentials(c.Request.Context(), req.AliyunCaptchaAccessKeyID, req.AliyunCaptchaAccessKeySecret, req.AliyunCaptchaSceneID, req.AliyunCaptchaRegion); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TOTP 双因素认证参数验证
|
||||
// 只有手动配置了加密密钥才允许启用 TOTP 功能
|
||||
if req.TotpEnabled && !previousSettings.TotpEnabled {
|
||||
@@ -1430,6 +1501,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
TencentCaptchaAppSecretKey: req.TencentCaptchaAppSecretKey,
|
||||
TencentCaptchaCloudSecretID: req.TencentCaptchaCloudSecretID,
|
||||
TencentCaptchaCloudSecretKey: req.TencentCaptchaCloudSecretKey,
|
||||
AliyunCaptchaEnabled: req.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: req.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecret: req.AliyunCaptchaAccessKeySecret,
|
||||
AliyunCaptchaSceneID: req.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: req.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: req.AliyunCaptchaRegion,
|
||||
APIKeyACLTrustForwardedIP: func() bool {
|
||||
if req.APIKeyACLTrustForwardedIP != nil {
|
||||
return *req.APIKeyACLTrustForwardedIP
|
||||
@@ -2007,6 +2084,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
TencentCaptchaAppSecretKeyConfigured: updatedSettings.TencentCaptchaAppSecretKeyConfigured,
|
||||
TencentCaptchaCloudSecretIDConfigured: updatedSettings.TencentCaptchaCloudSecretIDConfigured,
|
||||
TencentCaptchaCloudSecretKeyConfigured: updatedSettings.TencentCaptchaCloudSecretKeyConfigured,
|
||||
AliyunCaptchaEnabled: updatedSettings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaAccessKeyID: updatedSettings.AliyunCaptchaAccessKeyID,
|
||||
AliyunCaptchaAccessKeySecretConfigured: updatedSettings.AliyunCaptchaAccessKeySecretConfigured,
|
||||
AliyunCaptchaSceneID: updatedSettings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: updatedSettings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: updatedSettings.AliyunCaptchaRegion,
|
||||
APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
|
||||
ForwardedClientIPHeaders: updatedSettings.ForwardedClientIPHeaders,
|
||||
LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
|
||||
|
||||
@@ -113,7 +113,7 @@ func clearDingTalkCookie(c *gin.Context, name string, secure bool) {
|
||||
// DingTalkOAuthStart 启动 DingTalk Connect OAuth 登录流程。
|
||||
// GET /api/v1/auth/oauth/dingtalk/start?redirect=/dashboard&intent=login
|
||||
func (h *AuthHandler) DingTalkOAuthStart(c *gin.Context) {
|
||||
if !h.requireTencentCaptchaForOAuthLoginStart(c) {
|
||||
if !h.requireActionCaptchaForOAuthLoginStart(c) {
|
||||
return
|
||||
}
|
||||
cfg, err := h.getDingTalkOAuthConfig(c.Request.Context())
|
||||
|
||||
@@ -59,7 +59,7 @@ func (h *AuthHandler) CompleteGoogleOAuthRegistration(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) emailOAuthStart(c *gin.Context, provider string) {
|
||||
if !h.requireTencentCaptchaForOAuthLoginStart(c) {
|
||||
if !h.requireActionCaptchaForOAuthLoginStart(c) {
|
||||
return
|
||||
}
|
||||
cfg, err := h.getEmailOAuthConfig(c.Request.Context(), provider)
|
||||
|
||||
@@ -82,7 +82,7 @@ func (e *linuxDoTokenExchangeError) Error() string {
|
||||
// LinuxDoOAuthStart 启动 LinuxDo Connect OAuth 登录流程。
|
||||
// GET /api/v1/auth/oauth/linuxdo/start?redirect=/dashboard
|
||||
func (h *AuthHandler) LinuxDoOAuthStart(c *gin.Context) {
|
||||
if !h.requireTencentCaptchaForOAuthLoginStart(c) {
|
||||
if !h.requireActionCaptchaForOAuthLoginStart(c) {
|
||||
return
|
||||
}
|
||||
cfg, err := h.getLinuxDoOAuthConfig(c.Request.Context())
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
)
|
||||
|
||||
type oauthStartCaptchaRequest struct {
|
||||
// TurnstileToken 承载阿里云验证码的 captchaVerifyParam(复用既有请求字段名)
|
||||
TurnstileToken string `json:"turnstile_token"`
|
||||
TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
|
||||
TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
|
||||
}
|
||||
@@ -19,7 +21,7 @@ type oauthStartResponse struct {
|
||||
AuthorizeURL string `json:"authorize_url"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) requireTencentCaptchaForOAuthLoginStart(c *gin.Context) bool {
|
||||
func (h *AuthHandler) requireActionCaptchaForOAuthLoginStart(c *gin.Context) bool {
|
||||
if strings.HasSuffix(strings.TrimRight(c.Request.URL.Path, "/"), "/bind/start") {
|
||||
return true
|
||||
}
|
||||
@@ -28,7 +30,8 @@ func (h *AuthHandler) requireTencentCaptchaForOAuthLoginStart(c *gin.Context) bo
|
||||
if c.Request.Method == http.MethodPost {
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
}
|
||||
if err := h.authService.VerifyTencentCaptchaIfEnabled(c.Request.Context(), service.CaptchaProof{
|
||||
if err := h.authService.VerifyActionCaptchaIfEnabled(c.Request.Context(), service.CaptchaProof{
|
||||
TurnstileToken: req.TurnstileToken,
|
||||
TencentTicket: req.TencentCaptchaTicket,
|
||||
TencentRandstr: req.TencentCaptchaRandstr,
|
||||
}, ip.GetClientIP(c)); err != nil {
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestOAuthStartPostReturnsAuthorizeURLAfterTencentVerification(t *testing.T)
|
||||
)
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
require.True(t, handler.requireTencentCaptchaForOAuthLoginStart(c))
|
||||
require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
|
||||
respondOAuthStart(c, "https://provider.example/authorize")
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
@@ -143,7 +143,7 @@ func TestOAuthStartPostRequiresTencentProofWhenEnabled(t *testing.T) {
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/"+provider+"/start", strings.NewReader(`{}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
require.False(t, handler.requireTencentCaptchaForOAuthLoginStart(c))
|
||||
require.False(t, handler.requireActionCaptchaForOAuthLoginStart(c))
|
||||
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
require.Contains(t, recorder.Body.String(), "TENCENT_CAPTCHA_VERIFICATION_FAILED")
|
||||
require.Zero(t, verifier.calls)
|
||||
@@ -158,7 +158,7 @@ func TestOAuthBindingPathRemainsOutsideTencentGate(t *testing.T) {
|
||||
c, _ := gin.CreateTestContext(recorder)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/oidc/bind/start", nil)
|
||||
|
||||
require.True(t, handler.requireTencentCaptchaForOAuthLoginStart(c))
|
||||
require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func TestOAuthStartGetRemainsCompatibleWhenTencentDisabled(t *testing.T) {
|
||||
c, _ := gin.CreateTestContext(recorder)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/github/start", nil)
|
||||
|
||||
require.True(t, handler.requireTencentCaptchaForOAuthLoginStart(c))
|
||||
require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
|
||||
respondOAuthStart(c, "https://provider.example/authorize")
|
||||
|
||||
require.Equal(t, http.StatusFound, recorder.Code)
|
||||
|
||||
@@ -115,7 +115,7 @@ type oidcJWK struct {
|
||||
// OIDCOAuthStart 启动通用 OIDC OAuth 登录流程。
|
||||
// GET /api/v1/auth/oauth/oidc/start?redirect=/dashboard
|
||||
func (h *AuthHandler) OIDCOAuthStart(c *gin.Context) {
|
||||
if !h.requireTencentCaptchaForOAuthLoginStart(c) {
|
||||
if !h.requireActionCaptchaForOAuthLoginStart(c) {
|
||||
return
|
||||
}
|
||||
cfg, err := h.getOIDCOAuthConfig(c.Request.Context())
|
||||
|
||||
@@ -96,7 +96,7 @@ type wechatPaymentOAuthContext struct {
|
||||
// WeChatOAuthStart starts the WeChat OAuth login flow and stores the short-lived
|
||||
// browser cookies required by the rebuild pending-auth bridge.
|
||||
func (h *AuthHandler) WeChatOAuthStart(c *gin.Context) {
|
||||
if !h.requireTencentCaptchaForOAuthLoginStart(c) {
|
||||
if !h.requireActionCaptchaForOAuthLoginStart(c) {
|
||||
return
|
||||
}
|
||||
cfg, err := h.getWeChatOAuthConfig(c.Request.Context(), c.Query("mode"), c)
|
||||
|
||||
@@ -64,6 +64,12 @@ type SystemSettings struct {
|
||||
TencentCaptchaAppSecretKeyConfigured bool `json:"tencent_captcha_app_secret_key_configured"`
|
||||
TencentCaptchaCloudSecretIDConfigured bool `json:"tencent_captcha_cloud_secret_id_configured"`
|
||||
TencentCaptchaCloudSecretKeyConfigured bool `json:"tencent_captcha_cloud_secret_key_configured"`
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaAccessKeyID string `json:"aliyun_captcha_access_key_id"`
|
||||
AliyunCaptchaAccessKeySecretConfigured bool `json:"aliyun_captcha_access_key_secret_configured"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
APIKeyACLTrustForwardedIP bool `json:"api_key_acl_trust_forwarded_ip"`
|
||||
ForwardedClientIPHeaders []string `json:"forwarded_client_ip_headers"`
|
||||
|
||||
@@ -348,6 +354,10 @@ type PublicSettings struct {
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
|
||||
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
SiteName string `json:"site_name"`
|
||||
SiteLogo string `json:"site_logo"`
|
||||
SiteSubtitle string `json:"site_subtitle"`
|
||||
|
||||
@@ -47,6 +47,8 @@ type passkeyFinishRequest struct {
|
||||
}
|
||||
|
||||
type passkeyBeginLoginRequest struct {
|
||||
// TurnstileToken 承载阿里云验证码的 captchaVerifyParam(复用既有请求字段名)
|
||||
TurnstileToken string `json:"turnstile_token"`
|
||||
TencentCaptchaTicket string `json:"tencent_captcha_ticket"`
|
||||
TencentCaptchaRandstr string `json:"tencent_captcha_randstr"`
|
||||
}
|
||||
@@ -78,7 +80,8 @@ func (h *PasskeyHandler) BeginLogin(c *gin.Context) {
|
||||
}
|
||||
var req passkeyBeginLoginRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := h.authService.VerifyTencentCaptchaIfEnabled(c.Request.Context(), service.CaptchaProof{
|
||||
if err := h.authService.VerifyActionCaptchaIfEnabled(c.Request.Context(), service.CaptchaProof{
|
||||
TurnstileToken: req.TurnstileToken,
|
||||
TencentTicket: req.TencentCaptchaTicket,
|
||||
TencentRandstr: req.TencentCaptchaRandstr,
|
||||
}, ip.GetClientIP(c)); err != nil {
|
||||
|
||||
@@ -62,6 +62,10 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
SiteName: settings.SiteName,
|
||||
SiteLogo: settings.SiteLogo,
|
||||
SiteSubtitle: settings.SiteSubtitle,
|
||||
|
||||
@@ -157,9 +157,10 @@ func ProvideSettingHandler(settingService *service.SettingService, buildInfo Bui
|
||||
}
|
||||
|
||||
// ProvideAdminSettingHandler creates admin.SettingHandler with notification template APIs.
|
||||
func ProvideAdminSettingHandler(settingService *service.SettingService, emailService *service.EmailService, turnstileService *service.TurnstileService, opsService *service.OpsService, paymentConfigService *service.PaymentConfigService, paymentService *service.PaymentService, userAttributeService *service.UserAttributeService, notificationEmailService *service.NotificationEmailService, totpService *service.TotpService, userService *service.UserService) *admin.SettingHandler {
|
||||
func ProvideAdminSettingHandler(settingService *service.SettingService, emailService *service.EmailService, turnstileService *service.TurnstileService, aliyunCaptchaService *service.AliyunCaptchaService, opsService *service.OpsService, paymentConfigService *service.PaymentConfigService, paymentService *service.PaymentService, userAttributeService *service.UserAttributeService, notificationEmailService *service.NotificationEmailService, totpService *service.TotpService, userService *service.UserService) *admin.SettingHandler {
|
||||
h := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService, paymentConfigService, paymentService, userAttributeService)
|
||||
h.SetNotificationEmailService(notificationEmailService)
|
||||
h.SetAliyunCaptchaService(aliyunCaptchaService)
|
||||
h.SetStepUpDeps(totpService, userService)
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
captcha "github.com/alibabacloud-go/captcha-20230305/client"
|
||||
openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils"
|
||||
"github.com/alibabacloud-go/tea/dara"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
const aliyunCaptchaTimeoutMillis = 10_000
|
||||
|
||||
type aliyunCaptchaVerifier struct {
|
||||
protocol string // "HTTPS";测试注入 "HTTP" 指向 httptest.Server
|
||||
timeoutMillis int
|
||||
}
|
||||
|
||||
func NewAliyunCaptchaVerifier() service.AliyunCaptchaVerifier {
|
||||
return &aliyunCaptchaVerifier{
|
||||
protocol: "HTTPS",
|
||||
timeoutMillis: aliyunCaptchaTimeoutMillis,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyCaptcha 调用阿里云验证码 2.0 VerifyIntelligentCaptcha。
|
||||
// AK/SK 是可热更的后台设置,每次调用按当前凭证新建 client。
|
||||
func (v *aliyunCaptchaVerifier) VerifyCaptcha(ctx context.Context, cred service.AliyunCaptchaCredentials, captchaVerifyParam string) (*service.AliyunCaptchaVerifyResult, error) {
|
||||
client, err := captcha.NewClient(&openapiutil.Config{
|
||||
AccessKeyId: dara.String(cred.AccessKeyID),
|
||||
AccessKeySecret: dara.String(cred.AccessKeySecret),
|
||||
Endpoint: dara.String(cred.Endpoint),
|
||||
Protocol: dara.String(v.protocol),
|
||||
ConnectTimeout: dara.Int(v.timeoutMillis),
|
||||
ReadTimeout: dara.Int(v.timeoutMillis),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create aliyun captcha client: %w", err)
|
||||
}
|
||||
|
||||
request := &captcha.VerifyIntelligentCaptchaRequest{
|
||||
CaptchaVerifyParam: dara.String(captchaVerifyParam),
|
||||
SceneId: dara.String(cred.SceneID),
|
||||
}
|
||||
|
||||
response, err := client.VerifyIntelligentCaptchaWithContext(ctx, request, &dara.RuntimeOptions{})
|
||||
if err != nil {
|
||||
return nil, normalizeAliyunCaptchaError(err)
|
||||
}
|
||||
|
||||
result := &service.AliyunCaptchaVerifyResult{}
|
||||
if body := response.Body; body != nil && body.Result != nil {
|
||||
result.VerifyResult = dara.BoolValue(body.Result.VerifyResult)
|
||||
result.VerifyCode = dara.StringValue(body.Result.VerifyCode)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// normalizeAliyunCaptchaError 把 SDK 的两种错误类型归一化为 service.AliyunCaptchaAPIError,
|
||||
// 其余错误(网络/超时等)原样返回。
|
||||
func normalizeAliyunCaptchaError(err error) error {
|
||||
var teaErr *tea.SDKError
|
||||
if errors.As(err, &teaErr) {
|
||||
return &service.AliyunCaptchaAPIError{
|
||||
Code: tea.StringValue(teaErr.Code),
|
||||
Message: tea.StringValue(teaErr.Message),
|
||||
}
|
||||
}
|
||||
var daraErr *dara.SDKError
|
||||
if errors.As(err, &daraErr) {
|
||||
return &service.AliyunCaptchaAPIError{
|
||||
Code: dara.StringValue(daraErr.Code),
|
||||
Message: dara.StringValue(daraErr.Message),
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
// newAliyunCaptchaTestTarget 起一个假的阿里云端点,让真实 SDK 走完整的签名/序列化链路。
|
||||
func newAliyunCaptchaTestTarget(t *testing.T, handler http.HandlerFunc) (*aliyunCaptchaVerifier, service.AliyunCaptchaCredentials) {
|
||||
t.Helper()
|
||||
server := httptest.NewServer(handler)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
verifier := &aliyunCaptchaVerifier{protocol: "HTTP", timeoutMillis: 2_000}
|
||||
cred := service.AliyunCaptchaCredentials{
|
||||
AccessKeyID: "test-ak-id",
|
||||
AccessKeySecret: "test-ak-secret",
|
||||
SceneID: "scene-1",
|
||||
Endpoint: strings.TrimPrefix(server.URL, "http://"),
|
||||
}
|
||||
return verifier, cred
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaVerifier_VerifySuccess(t *testing.T) {
|
||||
var capturedParam, capturedSceneID string
|
||||
verifier, cred := newAliyunCaptchaTestTarget(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.NoError(t, r.ParseForm())
|
||||
capturedParam = r.Form.Get("CaptchaVerifyParam")
|
||||
capturedSceneID = r.Form.Get("SceneId")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"Code":"Success","Message":"success","RequestId":"req-1","Success":true,"Result":{"VerifyResult":true,"VerifyCode":"T001"}}`))
|
||||
})
|
||||
|
||||
result, err := verifier.VerifyCaptcha(context.Background(), cred, "the-verify-param")
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.VerifyResult)
|
||||
require.Equal(t, "T001", result.VerifyCode)
|
||||
require.Equal(t, "the-verify-param", capturedParam)
|
||||
require.Equal(t, "scene-1", capturedSceneID)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaVerifier_VerifyResultFalse(t *testing.T) {
|
||||
verifier, cred := newAliyunCaptchaTestTarget(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"Code":"Success","RequestId":"req-2","Success":true,"Result":{"VerifyResult":false,"VerifyCode":"F002"}}`))
|
||||
})
|
||||
|
||||
result, err := verifier.VerifyCaptcha(context.Background(), cred, "bad-param")
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.VerifyResult)
|
||||
require.Equal(t, "F002", result.VerifyCode)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaVerifier_APIErrorNormalized(t *testing.T) {
|
||||
verifier, cred := newAliyunCaptchaTestTarget(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte(`{"Code":"SignatureDoesNotMatch","Message":"Specified signature is not matched with our calculation.","RequestId":"req-3"}`))
|
||||
})
|
||||
|
||||
_, err := verifier.VerifyCaptcha(context.Background(), cred, "param")
|
||||
require.Error(t, err)
|
||||
var apiErr *service.AliyunCaptchaAPIError
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, "SignatureDoesNotMatch", apiErr.Code)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaVerifier_TransportError(t *testing.T) {
|
||||
server := httptest.NewServer(http.NotFoundHandler())
|
||||
endpoint := strings.TrimPrefix(server.URL, "http://")
|
||||
server.Close() // 立即关闭,制造连接失败
|
||||
|
||||
verifier := &aliyunCaptchaVerifier{protocol: "HTTP", timeoutMillis: 2_000}
|
||||
cred := service.AliyunCaptchaCredentials{
|
||||
AccessKeyID: "test-ak-id",
|
||||
AccessKeySecret: "test-ak-secret",
|
||||
SceneID: "scene-1",
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
|
||||
_, err := verifier.VerifyCaptcha(context.Background(), cred, "param")
|
||||
require.Error(t, err)
|
||||
var apiErr *service.AliyunCaptchaAPIError
|
||||
require.False(t, errors.As(err, &apiErr), "transport errors must not be normalized to API errors")
|
||||
}
|
||||
@@ -150,6 +150,7 @@ var ProviderSet = wire.NewSet(
|
||||
// HTTP service ports (DI Strategy A: return interface directly)
|
||||
NewTurnstileVerifier,
|
||||
NewTencentCaptchaVerifier,
|
||||
NewAliyunCaptchaVerifier,
|
||||
ProvidePricingRemoteClient,
|
||||
ProvideGitHubReleaseClient,
|
||||
NewProxyExitInfoProber,
|
||||
|
||||
@@ -746,6 +746,12 @@ func TestAPIContracts(t *testing.T) {
|
||||
"tencent_captcha_app_secret_key_configured": false,
|
||||
"tencent_captcha_cloud_secret_id_configured": false,
|
||||
"tencent_captcha_cloud_secret_key_configured": false,
|
||||
"aliyun_captcha_enabled": false,
|
||||
"aliyun_captcha_access_key_id": "",
|
||||
"aliyun_captcha_access_key_secret_configured": false,
|
||||
"aliyun_captcha_scene_id": "",
|
||||
"aliyun_captcha_prefix": "",
|
||||
"aliyun_captcha_region": "cn",
|
||||
"linuxdo_connect_enabled": false,
|
||||
"linuxdo_connect_client_id": "",
|
||||
"linuxdo_connect_client_secret_configured": false,
|
||||
@@ -1079,6 +1085,12 @@ func TestAPIContracts(t *testing.T) {
|
||||
"tencent_captcha_app_secret_key_configured": false,
|
||||
"tencent_captcha_cloud_secret_id_configured": false,
|
||||
"tencent_captcha_cloud_secret_key_configured": false,
|
||||
"aliyun_captcha_enabled": false,
|
||||
"aliyun_captcha_access_key_id": "",
|
||||
"aliyun_captcha_access_key_secret_configured": false,
|
||||
"aliyun_captcha_scene_id": "",
|
||||
"aliyun_captcha_prefix": "",
|
||||
"aliyun_captcha_region": "cn",
|
||||
"linuxdo_connect_enabled": false,
|
||||
"linuxdo_connect_client_id": "",
|
||||
"linuxdo_connect_client_secret_configured": false,
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAliyunCaptchaVerificationFailed = infraerrors.BadRequest("ALIYUN_CAPTCHA_VERIFICATION_FAILED", "aliyun captcha verification failed")
|
||||
ErrAliyunCaptchaNotConfigured = infraerrors.ServiceUnavailable("ALIYUN_CAPTCHA_NOT_CONFIGURED", "aliyun captcha not configured")
|
||||
// ErrCaptchaInvalidCredentials 阿里云验证码凭证无效(仅后台保存校验时返回,公开接口错误码不变)
|
||||
ErrCaptchaInvalidCredentials = infraerrors.BadRequest("CAPTCHA_INVALID_CREDENTIALS", "invalid aliyun captcha credentials")
|
||||
)
|
||||
|
||||
// AliyunCaptchaCredentials 阿里云验证码 2.0 服务端校验所需的完整凭证
|
||||
type AliyunCaptchaCredentials struct {
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
SceneID string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
// AliyunCaptchaVerifyResult VerifyIntelligentCaptcha 的归一化结果
|
||||
type AliyunCaptchaVerifyResult struct {
|
||||
VerifyResult bool
|
||||
VerifyCode string // 阿里云细分结果码,仅用于日志
|
||||
}
|
||||
|
||||
// AliyunCaptchaAPIError 阿里云 OpenAPI 业务错误。
|
||||
// repository 层负责把 SDK 错误归一化为该类型,service 层不依赖 SDK 包。
|
||||
type AliyunCaptchaAPIError struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *AliyunCaptchaAPIError) Error() string {
|
||||
return fmt.Sprintf("aliyun captcha api error: %s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// AliyunCaptchaVerifier 调用阿里云验证码 2.0 服务端校验的端口
|
||||
type AliyunCaptchaVerifier interface {
|
||||
VerifyCaptcha(ctx context.Context, cred AliyunCaptchaCredentials, captchaVerifyParam string) (*AliyunCaptchaVerifyResult, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// AliyunCaptchaRegionCN 中国内地;AliyunCaptchaRegionSGP 新加坡。
|
||||
// 该值同时下发给前端 AliyunCaptchaConfig.region,两端必须一致。
|
||||
AliyunCaptchaRegionCN = "cn"
|
||||
AliyunCaptchaRegionSGP = "sgp"
|
||||
|
||||
aliyunCaptchaEndpointCN = "captcha.cn-shanghai.aliyuncs.com"
|
||||
aliyunCaptchaEndpointSGP = "captcha.ap-southeast-1.aliyuncs.com"
|
||||
)
|
||||
|
||||
// aliyunCaptchaEndpoint 按后台配置的地域返回服务端接入点,未知值回退中国内地
|
||||
func aliyunCaptchaEndpoint(region string) string {
|
||||
if region == AliyunCaptchaRegionSGP {
|
||||
return aliyunCaptchaEndpointSGP
|
||||
}
|
||||
return aliyunCaptchaEndpointCN
|
||||
}
|
||||
|
||||
// normalizeAliyunCaptchaRegion 非法值一律视为中国内地
|
||||
func normalizeAliyunCaptchaRegion(value string) string {
|
||||
if value == AliyunCaptchaRegionSGP {
|
||||
return AliyunCaptchaRegionSGP
|
||||
}
|
||||
return AliyunCaptchaRegionCN
|
||||
}
|
||||
|
||||
// aliyunCredentialValidationParam 用于后台保存时探测凭证有效性的假验证参数
|
||||
const aliyunCredentialValidationParam = "sub2api-credential-validation"
|
||||
|
||||
// aliyunInvalidCredentialCodes 表示 AK/SK 本身无效的阿里云错误码;
|
||||
// 其余错误码(如 param 无效)说明签名已通过、凭证可用。
|
||||
var aliyunInvalidCredentialCodes = map[string]struct{}{
|
||||
"InvalidAccessKeyId.NotFound": {},
|
||||
"InvalidAccessKeyId.Inactive": {},
|
||||
"SignatureDoesNotMatch": {},
|
||||
"Forbidden.AccessKeyDisabled": {},
|
||||
"IncompleteSignature": {},
|
||||
"InvalidSecurityToken.Expired": {},
|
||||
}
|
||||
|
||||
// AliyunCaptchaService 阿里云验证码 2.0 服务端校验
|
||||
type AliyunCaptchaService struct {
|
||||
settingService *SettingService
|
||||
verifier AliyunCaptchaVerifier
|
||||
}
|
||||
|
||||
func NewAliyunCaptchaService(settingService *SettingService, verifier AliyunCaptchaVerifier) *AliyunCaptchaService {
|
||||
return &AliyunCaptchaService{settingService: settingService, verifier: verifier}
|
||||
}
|
||||
|
||||
func aliyunCaptchaCredentials(config AliyunCaptchaConfig) (AliyunCaptchaCredentials, bool) {
|
||||
cred := AliyunCaptchaCredentials{
|
||||
AccessKeyID: strings.TrimSpace(config.AccessKeyID),
|
||||
AccessKeySecret: strings.TrimSpace(config.AccessKeySecret),
|
||||
SceneID: strings.TrimSpace(config.SceneID),
|
||||
Endpoint: aliyunCaptchaEndpoint(config.Region),
|
||||
}
|
||||
if cred.AccessKeyID == "" || cred.AccessKeySecret == "" || cred.SceneID == "" {
|
||||
return AliyunCaptchaCredentials{}, false
|
||||
}
|
||||
return cred, true
|
||||
}
|
||||
|
||||
// VerifyParamWithConfig 校验阿里云验证码 2.0 的 captchaVerifyParam。
|
||||
// 调用异常时返回错误(fail-closed),与 Turnstile 网络错误行为对称。
|
||||
func (s *AliyunCaptchaService) VerifyParamWithConfig(ctx context.Context, config AliyunCaptchaConfig, captchaVerifyParam string) error {
|
||||
if s == nil || s.verifier == nil {
|
||||
return ErrAliyunCaptchaNotConfigured
|
||||
}
|
||||
cred, ok := aliyunCaptchaCredentials(config)
|
||||
if !ok {
|
||||
logger.LegacyPrintf("service.aliyun_captcha", "%s", "[AliyunCaptcha] credentials not configured")
|
||||
return ErrAliyunCaptchaNotConfigured
|
||||
}
|
||||
|
||||
if strings.TrimSpace(captchaVerifyParam) == "" {
|
||||
logger.LegacyPrintf("service.aliyun_captcha", "%s", "[AliyunCaptcha] captchaVerifyParam is empty")
|
||||
return ErrAliyunCaptchaVerificationFailed
|
||||
}
|
||||
|
||||
result, err := s.verifier.VerifyCaptcha(ctx, cred, captchaVerifyParam)
|
||||
if err != nil {
|
||||
logger.LegacyPrintf("service.aliyun_captcha", "[AliyunCaptcha] verify request failed: %v", err)
|
||||
return fmt.Errorf("%w: verifier request failed", ErrAliyunCaptchaVerificationFailed)
|
||||
}
|
||||
|
||||
if result == nil || !result.VerifyResult {
|
||||
if result != nil {
|
||||
logger.LegacyPrintf("service.aliyun_captcha", "[AliyunCaptcha] rejected, verify code: %s", result.VerifyCode)
|
||||
}
|
||||
return ErrAliyunCaptchaVerificationFailed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCredentials 用假验证参数探测阿里云 AK/SK 是否可用(后台保存设置时调用)。
|
||||
// 凭证类错误码返回 ErrCaptchaInvalidCredentials;正常响应(包括 param 无效导致的
|
||||
// VerifyResult=false)说明签名通过、凭证有效;其余错误原样返回给管理员排查。
|
||||
func (s *AliyunCaptchaService) ValidateCredentials(ctx context.Context, accessKeyID, accessKeySecret, sceneID, region string) error {
|
||||
if s.verifier == nil {
|
||||
return ErrAliyunCaptchaNotConfigured
|
||||
}
|
||||
cred := AliyunCaptchaCredentials{
|
||||
AccessKeyID: accessKeyID,
|
||||
AccessKeySecret: accessKeySecret,
|
||||
SceneID: sceneID,
|
||||
Endpoint: aliyunCaptchaEndpoint(region),
|
||||
}
|
||||
|
||||
_, err := s.verifier.VerifyCaptcha(ctx, cred, aliyunCredentialValidationParam)
|
||||
if err != nil {
|
||||
var apiErr *AliyunCaptchaAPIError
|
||||
if errors.As(err, &apiErr) {
|
||||
if _, invalid := aliyunInvalidCredentialCodes[apiErr.Code]; invalid {
|
||||
return ErrCaptchaInvalidCredentials
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("validate aliyun captcha credentials: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type aliyunVerifierSpy struct {
|
||||
called int
|
||||
lastCred AliyunCaptchaCredentials
|
||||
lastParam string
|
||||
result *AliyunCaptchaVerifyResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *aliyunVerifierSpy) VerifyCaptcha(_ context.Context, cred AliyunCaptchaCredentials, param string) (*AliyunCaptchaVerifyResult, error) {
|
||||
s.called++
|
||||
s.lastCred = cred
|
||||
s.lastParam = param
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
if s.result != nil {
|
||||
return s.result, nil
|
||||
}
|
||||
return &AliyunCaptchaVerifyResult{VerifyResult: true}, nil
|
||||
}
|
||||
|
||||
func aliyunEnabledSettings() map[string]string {
|
||||
return map[string]string{
|
||||
SettingKeyAliyunCaptchaEnabled: "true",
|
||||
SettingKeyAliyunCaptchaAccessKeyID: "ak-id",
|
||||
SettingKeyAliyunCaptchaAccessKeySecret: "ak-secret",
|
||||
SettingKeyAliyunCaptchaSceneID: "scene-1",
|
||||
SettingKeyAliyunCaptchaPrefix: "prefix-1",
|
||||
}
|
||||
}
|
||||
|
||||
func aliyunTestConfig() AliyunCaptchaConfig {
|
||||
return AliyunCaptchaConfig{
|
||||
Enabled: true,
|
||||
AccessKeyID: "ak-id",
|
||||
AccessKeySecret: "ak-secret",
|
||||
SceneID: "scene-1",
|
||||
Region: AliyunCaptchaRegionCN,
|
||||
}
|
||||
}
|
||||
|
||||
func newAliyunAuthServiceForTest(cfg *config.Config, settings map[string]string, aliyunSpy *aliyunVerifierSpy) *AuthService {
|
||||
settingService := NewSettingService(&settingPublicRepoStub{values: settings}, cfg)
|
||||
authService := NewAuthService(
|
||||
nil, // entClient
|
||||
nil, // userRepo
|
||||
nil, // redeemRepo
|
||||
nil, // refreshTokenCache
|
||||
cfg,
|
||||
settingService,
|
||||
nil, // emailService
|
||||
NewTurnstileService(settingService, &turnstileVerifierSpy{}),
|
||||
nil, // emailQueueService
|
||||
nil, // promoService
|
||||
nil, // defaultSubAssigner
|
||||
nil, // affiliateService
|
||||
nil, // userPlatformQuotaRepo
|
||||
)
|
||||
authService.SetAliyunCaptchaService(NewAliyunCaptchaService(settingService, aliyunSpy))
|
||||
return authService
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaServiceVerifyParamDispatch(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
|
||||
err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "captcha-verify-param")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, spy.called)
|
||||
require.Equal(t, "captcha-verify-param", spy.lastParam)
|
||||
require.Equal(t, "ak-id", spy.lastCred.AccessKeyID)
|
||||
require.Equal(t, "scene-1", spy.lastCred.SceneID)
|
||||
require.Equal(t, "captcha.cn-shanghai.aliyuncs.com", spy.lastCred.Endpoint)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaServiceSgpEndpoint(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
cfg := aliyunTestConfig()
|
||||
cfg.Region = AliyunCaptchaRegionSGP
|
||||
|
||||
err := svc.VerifyParamWithConfig(context.Background(), cfg, "captcha-verify-param")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "captcha.ap-southeast-1.aliyuncs.com", spy.lastCred.Endpoint)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaServiceFailsClosedOnVerifierError(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{err: errors.New("network down")}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
|
||||
err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "captcha-verify-param")
|
||||
|
||||
require.ErrorIs(t, err, ErrAliyunCaptchaVerificationFailed)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaServiceRejectsVerifyResultFalse(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{result: &AliyunCaptchaVerifyResult{VerifyResult: false, VerifyCode: "F001"}}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
|
||||
err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "captcha-verify-param")
|
||||
|
||||
require.ErrorIs(t, err, ErrAliyunCaptchaVerificationFailed)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaServiceRejectsIncompleteCredentials(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
cfg := aliyunTestConfig()
|
||||
cfg.AccessKeySecret = ""
|
||||
|
||||
err := svc.VerifyParamWithConfig(context.Background(), cfg, "captcha-verify-param")
|
||||
|
||||
require.ErrorIs(t, err, ErrAliyunCaptchaNotConfigured)
|
||||
require.Zero(t, spy.called)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaServiceRejectsEmptyParam(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
|
||||
err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "")
|
||||
|
||||
require.ErrorIs(t, err, ErrAliyunCaptchaVerificationFailed)
|
||||
require.Zero(t, spy.called)
|
||||
}
|
||||
|
||||
func TestAliyunCaptchaServiceValidateCredentials(t *testing.T) {
|
||||
t.Run("invalid credential code", func(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{err: &AliyunCaptchaAPIError{Code: "SignatureDoesNotMatch", Message: "bad sk"}}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
|
||||
err := svc.ValidateCredentials(context.Background(), "id", "sk", "scene", "cn")
|
||||
require.ErrorIs(t, err, ErrCaptchaInvalidCredentials)
|
||||
})
|
||||
|
||||
t.Run("network error surfaces", func(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{err: errors.New("timeout")}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
|
||||
err := svc.ValidateCredentials(context.Background(), "id", "sk", "scene", "cn")
|
||||
require.Error(t, err)
|
||||
require.NotErrorIs(t, err, ErrCaptchaInvalidCredentials)
|
||||
})
|
||||
|
||||
t.Run("verify result false means credentials valid", func(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{result: &AliyunCaptchaVerifyResult{VerifyResult: false}}
|
||||
svc := NewAliyunCaptchaService(nil, spy)
|
||||
|
||||
err := svc.ValidateCredentials(context.Background(), "id", "sk", "scene", "sgp")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "captcha.ap-southeast-1.aliyuncs.com", spy.lastCred.Endpoint)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthServiceVerifyCaptchaDispatchesAliyun(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{}
|
||||
authService := newAliyunAuthServiceForTest(&config.Config{}, aliyunEnabledSettings(), spy)
|
||||
|
||||
// 阿里云 captchaVerifyParam 复用 turnstile_token 请求字段
|
||||
err := authService.VerifyCaptcha(context.Background(), CaptchaProof{TurnstileToken: "captcha-verify-param"}, "127.0.0.1")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, spy.called)
|
||||
require.Equal(t, "captcha-verify-param", spy.lastParam)
|
||||
}
|
||||
|
||||
func TestAuthServiceVerifyCaptchaRejectsProviderConflict(t *testing.T) {
|
||||
settings := aliyunEnabledSettings()
|
||||
settings[SettingKeyTurnstileEnabled] = "true"
|
||||
settings[SettingKeyTurnstileSecretKey] = "secret"
|
||||
spy := &aliyunVerifierSpy{}
|
||||
authService := newAliyunAuthServiceForTest(&config.Config{}, settings, spy)
|
||||
|
||||
err := authService.VerifyCaptcha(context.Background(), CaptchaProof{TurnstileToken: "param"}, "127.0.0.1")
|
||||
|
||||
require.ErrorIs(t, err, ErrCaptchaProviderConflict)
|
||||
require.Zero(t, spy.called)
|
||||
}
|
||||
|
||||
func TestAuthServiceVerifyCaptchaRequiredModeWithAliyun(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{Mode: "release"},
|
||||
Turnstile: config.TurnstileConfig{Required: true},
|
||||
}
|
||||
spy := &aliyunVerifierSpy{}
|
||||
authService := newAliyunAuthServiceForTest(cfg, aliyunEnabledSettings(), spy)
|
||||
|
||||
// required 模式 + 阿里云启用且凭证齐全:不误报 NOT_CONFIGURED,正常走阿里云校验
|
||||
err := authService.VerifyCaptcha(context.Background(), CaptchaProof{TurnstileToken: "captcha-verify-param"}, "127.0.0.1")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, spy.called)
|
||||
}
|
||||
|
||||
func TestAuthServiceVerifyActionCaptchaIfEnabledDispatchesAliyun(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{}
|
||||
authService := newAliyunAuthServiceForTest(&config.Config{}, aliyunEnabledSettings(), spy)
|
||||
|
||||
err := authService.VerifyActionCaptchaIfEnabled(context.Background(), CaptchaProof{TurnstileToken: "captcha-verify-param"}, "127.0.0.1")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, spy.called)
|
||||
require.Equal(t, "captcha-verify-param", spy.lastParam)
|
||||
}
|
||||
|
||||
func TestAuthServiceVerifyActionCaptchaIfEnabledSkipsWhenOnlyTurnstile(t *testing.T) {
|
||||
spy := &aliyunVerifierSpy{}
|
||||
authService := newAliyunAuthServiceForTest(&config.Config{}, map[string]string{
|
||||
SettingKeyTurnstileEnabled: "true",
|
||||
SettingKeyTurnstileSecretKey: "secret",
|
||||
}, spy)
|
||||
|
||||
// Turnstile 不扩大既有覆盖:扩展入口不拦截
|
||||
err := authService.VerifyActionCaptchaIfEnabled(context.Background(), CaptchaProof{}, "127.0.0.1")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, spy.called)
|
||||
}
|
||||
@@ -76,6 +76,7 @@ type AuthService struct {
|
||||
emailService *EmailService
|
||||
turnstileService *TurnstileService
|
||||
tencentCaptchaService *TencentCaptchaService
|
||||
aliyunCaptchaService *AliyunCaptchaService
|
||||
emailQueueService *EmailQueueService
|
||||
promoService *PromoService
|
||||
affiliateService *AffiliateService
|
||||
@@ -84,6 +85,7 @@ type AuthService struct {
|
||||
}
|
||||
|
||||
type CaptchaProof struct {
|
||||
// TurnstileToken 承载 Cloudflare Turnstile token;阿里云验证码复用该字段承载 captchaVerifyParam
|
||||
TurnstileToken string
|
||||
TencentTicket string
|
||||
TencentRandstr string
|
||||
@@ -144,6 +146,10 @@ func (s *AuthService) SetTencentCaptchaService(tencentCaptchaService *TencentCap
|
||||
s.tencentCaptchaService = tencentCaptchaService
|
||||
}
|
||||
|
||||
func (s *AuthService) SetAliyunCaptchaService(aliyunCaptchaService *AliyunCaptchaService) {
|
||||
s.aliyunCaptchaService = aliyunCaptchaService
|
||||
}
|
||||
|
||||
// Register 用户注册,返回token和用户
|
||||
func (s *AuthService) Register(ctx context.Context, email, password string) (string, *User, error) {
|
||||
return s.RegisterWithVerification(ctx, email, password, "", "", "", "")
|
||||
@@ -412,7 +418,8 @@ func (s *AuthService) VerifyCaptcha(ctx context.Context, proof CaptchaProof, rem
|
||||
}
|
||||
turnstileEnabled := providerConfig.TurnstileEnabled
|
||||
tencentEnabled := providerConfig.Tencent.Enabled
|
||||
if turnstileEnabled && tencentEnabled {
|
||||
aliyunEnabled := providerConfig.Aliyun.Enabled
|
||||
if captchaProvidersConflict(turnstileEnabled, tencentEnabled, aliyunEnabled) {
|
||||
return ErrCaptchaProviderConflict
|
||||
}
|
||||
if tencentEnabled {
|
||||
@@ -421,6 +428,12 @@ func (s *AuthService) VerifyCaptcha(ctx context.Context, proof CaptchaProof, rem
|
||||
}
|
||||
return s.tencentCaptchaService.VerifyTicketWithConfig(ctx, providerConfig.Tencent, proof.TencentTicket, proof.TencentRandstr, remoteIP)
|
||||
}
|
||||
if aliyunEnabled {
|
||||
if s.aliyunCaptchaService == nil {
|
||||
return ErrAliyunCaptchaNotConfigured
|
||||
}
|
||||
return s.aliyunCaptchaService.VerifyParamWithConfig(ctx, providerConfig.Aliyun, proof.TurnstileToken)
|
||||
}
|
||||
if turnstileEnabled {
|
||||
if s.turnstileService == nil || strings.TrimSpace(providerConfig.TurnstileSecretKey) == "" {
|
||||
return ErrTurnstileNotConfigured
|
||||
@@ -433,9 +446,20 @@ func (s *AuthService) VerifyCaptcha(ctx context.Context, proof CaptchaProof, rem
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTencentCaptchaIfEnabled 仅保护新增的腾讯验证码动作入口,
|
||||
// 不扩大 Cloudflare Turnstile 的既有覆盖范围。
|
||||
func (s *AuthService) VerifyTencentCaptchaIfEnabled(ctx context.Context, proof CaptchaProof, remoteIP string) error {
|
||||
// captchaProvidersConflict 同一时间仅允许启用一家人机验证服务商
|
||||
func captchaProvidersConflict(enabled ...bool) bool {
|
||||
count := 0
|
||||
for _, e := range enabled {
|
||||
if e {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count > 1
|
||||
}
|
||||
|
||||
// VerifyActionCaptchaIfEnabled 仅保护动作触发的扩展入口(OAuth 登录启动、passkey 登录),
|
||||
// 腾讯天御与阿里云验证码启用时拦截;不扩大 Cloudflare Turnstile 的既有覆盖范围。
|
||||
func (s *AuthService) VerifyActionCaptchaIfEnabled(ctx context.Context, proof CaptchaProof, remoteIP string) error {
|
||||
if s == nil || s.settingService == nil {
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
@@ -445,12 +469,20 @@ func (s *AuthService) VerifyTencentCaptchaIfEnabled(ctx context.Context, proof C
|
||||
logger.LegacyPrintf("service.auth", "%s", "[Auth] Failed to read captcha provider settings")
|
||||
return ErrServiceUnavailable
|
||||
}
|
||||
if !providerConfig.Tencent.Enabled {
|
||||
tencentEnabled := providerConfig.Tencent.Enabled
|
||||
aliyunEnabled := providerConfig.Aliyun.Enabled
|
||||
if !tencentEnabled && !aliyunEnabled {
|
||||
return nil
|
||||
}
|
||||
if providerConfig.TurnstileEnabled {
|
||||
if captchaProvidersConflict(providerConfig.TurnstileEnabled, tencentEnabled, aliyunEnabled) {
|
||||
return ErrCaptchaProviderConflict
|
||||
}
|
||||
if aliyunEnabled {
|
||||
if s.aliyunCaptchaService == nil {
|
||||
return ErrAliyunCaptchaNotConfigured
|
||||
}
|
||||
return s.aliyunCaptchaService.VerifyParamWithConfig(ctx, providerConfig.Aliyun, proof.TurnstileToken)
|
||||
}
|
||||
if s.tencentCaptchaService == nil {
|
||||
return ErrTencentCaptchaNotConfigured
|
||||
}
|
||||
|
||||
@@ -150,11 +150,11 @@ func TestVerifyCaptchaRejectsEnabledTencentProviderWithIncompleteCredentials(t *
|
||||
require.Zero(t, verifier.calls)
|
||||
}
|
||||
|
||||
func TestVerifyTencentCaptchaIfEnabledVerifiesTencentProof(t *testing.T) {
|
||||
func TestVerifyActionCaptchaIfEnabledVerifiesTencentProof(t *testing.T) {
|
||||
verifier := &tencentCaptchaVerifierStub{response: &TencentCaptchaVerifyResponse{CaptchaCode: 1}}
|
||||
svc := newAuthServiceForCaptchaTest(tencentCaptchaSettings(), false, nil, verifier)
|
||||
|
||||
err := svc.VerifyTencentCaptchaIfEnabled(context.Background(), CaptchaProof{
|
||||
err := svc.VerifyActionCaptchaIfEnabled(context.Background(), CaptchaProof{
|
||||
TencentTicket: "ticket",
|
||||
TencentRandstr: "@rand",
|
||||
}, "203.0.113.10")
|
||||
@@ -164,7 +164,7 @@ func TestVerifyTencentCaptchaIfEnabledVerifiesTencentProof(t *testing.T) {
|
||||
require.Equal(t, TencentCaptchaProof{Ticket: "ticket", Randstr: "@rand"}, verifier.proof)
|
||||
}
|
||||
|
||||
func TestVerifyTencentCaptchaIfEnabledDoesNotExpandTurnstileCoverage(t *testing.T) {
|
||||
func TestVerifyActionCaptchaIfEnabledDoesNotExpandTurnstileCoverage(t *testing.T) {
|
||||
settings := map[string]string{
|
||||
SettingKeyTurnstileEnabled: "true",
|
||||
SettingKeyTurnstileSecretKey: "turnstile-secret",
|
||||
@@ -172,17 +172,17 @@ func TestVerifyTencentCaptchaIfEnabledDoesNotExpandTurnstileCoverage(t *testing.
|
||||
turnstileVerifier := &turnstileVerifierSpy{}
|
||||
svc := newAuthServiceForCaptchaTest(settings, false, turnstileVerifier, nil)
|
||||
|
||||
err := svc.VerifyTencentCaptchaIfEnabled(context.Background(), CaptchaProof{}, "203.0.113.10")
|
||||
err := svc.VerifyActionCaptchaIfEnabled(context.Background(), CaptchaProof{}, "203.0.113.10")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, turnstileVerifier.called)
|
||||
}
|
||||
|
||||
func TestVerifyTencentCaptchaIfEnabledFailsClosedOnSettingReadError(t *testing.T) {
|
||||
func TestVerifyActionCaptchaIfEnabledFailsClosedOnSettingReadError(t *testing.T) {
|
||||
repo := &settingRepoStub{err: errors.New("settings unavailable")}
|
||||
svc := newAuthServiceForCaptchaRepoTest(repo, false, &turnstileVerifierSpy{}, &tencentCaptchaVerifierStub{})
|
||||
|
||||
err := svc.VerifyTencentCaptchaIfEnabled(context.Background(), CaptchaProof{}, "203.0.113.10")
|
||||
err := svc.VerifyActionCaptchaIfEnabled(context.Background(), CaptchaProof{}, "203.0.113.10")
|
||||
|
||||
require.ErrorIs(t, err, ErrServiceUnavailable)
|
||||
}
|
||||
|
||||
@@ -171,6 +171,14 @@ const (
|
||||
SettingKeyTencentCaptchaCloudSecretID = "tencent_captcha_cloud_secret_id"
|
||||
SettingKeyTencentCaptchaCloudSecretKey = "tencent_captcha_cloud_secret_key"
|
||||
|
||||
// 阿里云验证码 2.0 设置(与 Turnstile、腾讯天御互斥,同一时间仅可启用一家)
|
||||
SettingKeyAliyunCaptchaEnabled = "aliyun_captcha_enabled" // 是否启用阿里云验证码
|
||||
SettingKeyAliyunCaptchaAccessKeyID = "aliyun_captcha_access_key_id" // 阿里云 AccessKey ID
|
||||
SettingKeyAliyunCaptchaAccessKeySecret = "aliyun_captcha_access_key_secret" // 阿里云 AccessKey Secret
|
||||
SettingKeyAliyunCaptchaSceneID = "aliyun_captcha_scene_id" // 验证场景 ID(所有认证流程共用)
|
||||
SettingKeyAliyunCaptchaPrefix = "aliyun_captcha_prefix" // 身份标,前端 SDK 初始化用
|
||||
SettingKeyAliyunCaptchaRegion = "aliyun_captcha_region" // 地域:"cn"|"sgp",决定前端脚本区域与服务端接入点
|
||||
|
||||
// API Key IP 访问控制设置
|
||||
SettingKeyAPIKeyACLTrustForwardedIP = "api_key_acl_trust_forwarded_ip" // API Key IP 白/黑名单是否信任转发 IP
|
||||
SettingKeyForwardedClientIPHeaders = "forwarded_client_ip_headers" // 自定义 CDN 客户端 IP 请求头(JSON 数组)
|
||||
|
||||
@@ -465,10 +465,21 @@ type TencentCaptchaConfig struct {
|
||||
CloudSecretKey string
|
||||
}
|
||||
|
||||
// AliyunCaptchaConfig contains the credentials required by Aliyun Captcha 2.0's
|
||||
// server-side verification API. It must never be returned by a public handler.
|
||||
type AliyunCaptchaConfig struct {
|
||||
Enabled bool
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
SceneID string
|
||||
Region string
|
||||
}
|
||||
|
||||
type CaptchaProviderConfig struct {
|
||||
TurnstileEnabled bool
|
||||
TurnstileSecretKey string
|
||||
Tencent TencentCaptchaConfig
|
||||
Aliyun AliyunCaptchaConfig
|
||||
}
|
||||
|
||||
func (s *SettingService) GetCaptchaProviderConfig(ctx context.Context) (CaptchaProviderConfig, error) {
|
||||
@@ -480,6 +491,11 @@ func (s *SettingService) GetCaptchaProviderConfig(ctx context.Context) (CaptchaP
|
||||
SettingKeyTencentCaptchaAppSecretKey,
|
||||
SettingKeyTencentCaptchaCloudSecretID,
|
||||
SettingKeyTencentCaptchaCloudSecretKey,
|
||||
SettingKeyAliyunCaptchaEnabled,
|
||||
SettingKeyAliyunCaptchaAccessKeyID,
|
||||
SettingKeyAliyunCaptchaAccessKeySecret,
|
||||
SettingKeyAliyunCaptchaSceneID,
|
||||
SettingKeyAliyunCaptchaRegion,
|
||||
})
|
||||
if err != nil {
|
||||
return CaptchaProviderConfig{}, fmt.Errorf("read captcha provider settings: %w", err)
|
||||
@@ -494,6 +510,13 @@ func (s *SettingService) GetCaptchaProviderConfig(ctx context.Context) (CaptchaP
|
||||
CloudSecretID: values[SettingKeyTencentCaptchaCloudSecretID],
|
||||
CloudSecretKey: values[SettingKeyTencentCaptchaCloudSecretKey],
|
||||
},
|
||||
Aliyun: AliyunCaptchaConfig{
|
||||
Enabled: values[SettingKeyAliyunCaptchaEnabled] == "true",
|
||||
AccessKeyID: values[SettingKeyAliyunCaptchaAccessKeyID],
|
||||
AccessKeySecret: values[SettingKeyAliyunCaptchaAccessKeySecret],
|
||||
SceneID: values[SettingKeyAliyunCaptchaSceneID],
|
||||
Region: normalizeAliyunCaptchaRegion(values[SettingKeyAliyunCaptchaRegion]),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -329,6 +329,12 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
TencentCaptchaAppSecretKeyConfigured: settings[SettingKeyTencentCaptchaAppSecretKey] != "",
|
||||
TencentCaptchaCloudSecretIDConfigured: settings[SettingKeyTencentCaptchaCloudSecretID] != "",
|
||||
TencentCaptchaCloudSecretKeyConfigured: settings[SettingKeyTencentCaptchaCloudSecretKey] != "",
|
||||
AliyunCaptchaEnabled: settings[SettingKeyAliyunCaptchaEnabled] == "true",
|
||||
AliyunCaptchaAccessKeyID: settings[SettingKeyAliyunCaptchaAccessKeyID],
|
||||
AliyunCaptchaAccessKeySecretConfigured: settings[SettingKeyAliyunCaptchaAccessKeySecret] != "",
|
||||
AliyunCaptchaSceneID: settings[SettingKeyAliyunCaptchaSceneID],
|
||||
AliyunCaptchaPrefix: settings[SettingKeyAliyunCaptchaPrefix],
|
||||
AliyunCaptchaRegion: normalizeAliyunCaptchaRegion(settings[SettingKeyAliyunCaptchaRegion]),
|
||||
APIKeyACLTrustForwardedIP: apiKeyACLTrustForwardedIP,
|
||||
ForwardedClientIPHeaders: forwardedClientIPHeaders,
|
||||
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
||||
@@ -403,6 +409,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
result.TencentCaptchaAppSecretKey = settings[SettingKeyTencentCaptchaAppSecretKey]
|
||||
result.TencentCaptchaCloudSecretID = settings[SettingKeyTencentCaptchaCloudSecretID]
|
||||
result.TencentCaptchaCloudSecretKey = settings[SettingKeyTencentCaptchaCloudSecretKey]
|
||||
result.AliyunCaptchaAccessKeySecret = settings[SettingKeyAliyunCaptchaAccessKeySecret]
|
||||
|
||||
// LinuxDo Connect 设置:
|
||||
// - 兼容 config.yaml/env(避免老部署因为未迁移到数据库设置而被意外关闭)
|
||||
|
||||
@@ -173,6 +173,10 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
SettingKeyTurnstileSiteKey,
|
||||
SettingKeyTencentCaptchaEnabled,
|
||||
SettingKeyTencentCaptchaAppID,
|
||||
SettingKeyAliyunCaptchaEnabled,
|
||||
SettingKeyAliyunCaptchaSceneID,
|
||||
SettingKeyAliyunCaptchaPrefix,
|
||||
SettingKeyAliyunCaptchaRegion,
|
||||
SettingKeyAPIKeyACLTrustForwardedIP,
|
||||
SettingKeySiteName,
|
||||
SettingKeySiteLogo,
|
||||
@@ -305,6 +309,10 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
||||
TencentCaptchaEnabled: settings[SettingKeyTencentCaptchaEnabled] == "true",
|
||||
TencentCaptchaAppID: settings[SettingKeyTencentCaptchaAppID],
|
||||
AliyunCaptchaEnabled: settings[SettingKeyAliyunCaptchaEnabled] == "true",
|
||||
AliyunCaptchaSceneID: settings[SettingKeyAliyunCaptchaSceneID],
|
||||
AliyunCaptchaPrefix: settings[SettingKeyAliyunCaptchaPrefix],
|
||||
AliyunCaptchaRegion: normalizeAliyunCaptchaRegion(settings[SettingKeyAliyunCaptchaRegion]),
|
||||
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
||||
SiteLogo: settings[SettingKeySiteLogo],
|
||||
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
||||
@@ -496,6 +504,10 @@ type PublicSettingsInjectionPayload struct {
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
TencentCaptchaEnabled bool `json:"tencent_captcha_enabled"`
|
||||
TencentCaptchaAppID string `json:"tencent_captcha_app_id"`
|
||||
AliyunCaptchaEnabled bool `json:"aliyun_captcha_enabled"`
|
||||
AliyunCaptchaSceneID string `json:"aliyun_captcha_scene_id"`
|
||||
AliyunCaptchaPrefix string `json:"aliyun_captcha_prefix"`
|
||||
AliyunCaptchaRegion string `json:"aliyun_captcha_region"`
|
||||
SiteName string `json:"site_name"`
|
||||
SiteLogo string `json:"site_logo"`
|
||||
SiteSubtitle string `json:"site_subtitle"`
|
||||
@@ -571,6 +583,10 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||
TencentCaptchaEnabled: settings.TencentCaptchaEnabled,
|
||||
TencentCaptchaAppID: settings.TencentCaptchaAppID,
|
||||
AliyunCaptchaEnabled: settings.AliyunCaptchaEnabled,
|
||||
AliyunCaptchaSceneID: settings.AliyunCaptchaSceneID,
|
||||
AliyunCaptchaPrefix: settings.AliyunCaptchaPrefix,
|
||||
AliyunCaptchaRegion: settings.AliyunCaptchaRegion,
|
||||
SiteName: settings.SiteName,
|
||||
SiteLogo: settings.SiteLogo,
|
||||
SiteSubtitle: settings.SiteSubtitle,
|
||||
|
||||
@@ -221,6 +221,15 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
||||
if settings.TencentCaptchaCloudSecretKey != "" {
|
||||
updates[SettingKeyTencentCaptchaCloudSecretKey] = settings.TencentCaptchaCloudSecretKey
|
||||
}
|
||||
// 阿里云验证码 2.0 设置(只有非空才更新密钥)
|
||||
updates[SettingKeyAliyunCaptchaEnabled] = strconv.FormatBool(settings.AliyunCaptchaEnabled)
|
||||
updates[SettingKeyAliyunCaptchaAccessKeyID] = settings.AliyunCaptchaAccessKeyID
|
||||
if settings.AliyunCaptchaAccessKeySecret != "" {
|
||||
updates[SettingKeyAliyunCaptchaAccessKeySecret] = settings.AliyunCaptchaAccessKeySecret
|
||||
}
|
||||
updates[SettingKeyAliyunCaptchaSceneID] = settings.AliyunCaptchaSceneID
|
||||
updates[SettingKeyAliyunCaptchaPrefix] = settings.AliyunCaptchaPrefix
|
||||
updates[SettingKeyAliyunCaptchaRegion] = normalizeAliyunCaptchaRegion(settings.AliyunCaptchaRegion)
|
||||
updates[SettingKeyAPIKeyACLTrustForwardedIP] = strconv.FormatBool(settings.APIKeyACLTrustForwardedIP)
|
||||
forwardedClientIPHeadersJSON, err := json.Marshal(settings.ForwardedClientIPHeaders)
|
||||
if err != nil {
|
||||
|
||||
@@ -50,6 +50,13 @@ type SystemSettings struct {
|
||||
TencentCaptchaCloudSecretIDConfigured bool
|
||||
TencentCaptchaCloudSecretKey string
|
||||
TencentCaptchaCloudSecretKeyConfigured bool
|
||||
AliyunCaptchaEnabled bool
|
||||
AliyunCaptchaAccessKeyID string
|
||||
AliyunCaptchaAccessKeySecret string
|
||||
AliyunCaptchaAccessKeySecretConfigured bool
|
||||
AliyunCaptchaSceneID string
|
||||
AliyunCaptchaPrefix string
|
||||
AliyunCaptchaRegion string
|
||||
APIKeyACLTrustForwardedIP bool
|
||||
ForwardedClientIPHeaders []string
|
||||
|
||||
@@ -312,6 +319,10 @@ type PublicSettings struct {
|
||||
TurnstileSiteKey string
|
||||
TencentCaptchaEnabled bool
|
||||
TencentCaptchaAppID string
|
||||
AliyunCaptchaEnabled bool
|
||||
AliyunCaptchaSceneID string
|
||||
AliyunCaptchaPrefix string
|
||||
AliyunCaptchaRegion string
|
||||
SiteName string
|
||||
SiteLogo string
|
||||
SiteSubtitle string
|
||||
|
||||
@@ -53,6 +53,7 @@ func ProvideAuthService(
|
||||
emailService *EmailService,
|
||||
turnstileService *TurnstileService,
|
||||
tencentCaptchaService *TencentCaptchaService,
|
||||
aliyunCaptchaService *AliyunCaptchaService,
|
||||
emailQueueService *EmailQueueService,
|
||||
promoService *PromoService,
|
||||
defaultSubAssigner DefaultSubscriptionAssigner,
|
||||
@@ -75,6 +76,7 @@ func ProvideAuthService(
|
||||
userPlatformQuotaRepo,
|
||||
)
|
||||
svc.SetTencentCaptchaService(tencentCaptchaService)
|
||||
svc.SetAliyunCaptchaService(aliyunCaptchaService)
|
||||
return svc
|
||||
}
|
||||
|
||||
@@ -799,6 +801,7 @@ var ProviderSet = wire.NewSet(
|
||||
ProvideEmailQueueService,
|
||||
NewTurnstileService,
|
||||
NewTencentCaptchaService,
|
||||
NewAliyunCaptchaService,
|
||||
NewSubscriptionService,
|
||||
wire.Bind(new(DefaultSubscriptionAssigner), new(*SubscriptionService)),
|
||||
ProvideConcurrencyService,
|
||||
|
||||
@@ -181,7 +181,7 @@ security:
|
||||
# 默认 CSP 策略(如果静态资源托管在其他域名,请自行覆盖)
|
||||
# Note: __CSP_NONCE__ will be replaced with 'nonce-xxx' at request time for inline script security
|
||||
# 注意:__CSP_NONCE__ 会在请求时被替换为 'nonce-xxx',用于内联脚本安全
|
||||
policy: "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
policy: "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
proxy_probe:
|
||||
# Allow skipping TLS verification for proxy probe (debug only)
|
||||
# 允许代理探测时跳过 TLS 证书验证(仅用于调试)
|
||||
|
||||
@@ -464,6 +464,12 @@ export interface SystemSettings {
|
||||
tencent_captcha_app_secret_key_configured: boolean;
|
||||
tencent_captcha_cloud_secret_id_configured: boolean;
|
||||
tencent_captcha_cloud_secret_key_configured: boolean;
|
||||
aliyun_captcha_enabled: boolean;
|
||||
aliyun_captcha_access_key_id: string;
|
||||
aliyun_captcha_access_key_secret_configured: boolean;
|
||||
aliyun_captcha_scene_id: string;
|
||||
aliyun_captcha_prefix: string;
|
||||
aliyun_captcha_region: string;
|
||||
api_key_acl_trust_forwarded_ip: boolean;
|
||||
forwarded_client_ip_headers: string[];
|
||||
|
||||
@@ -783,6 +789,12 @@ export interface UpdateSettingsRequest {
|
||||
tencent_captcha_app_secret_key?: string;
|
||||
tencent_captcha_cloud_secret_id?: string;
|
||||
tencent_captcha_cloud_secret_key?: string;
|
||||
aliyun_captcha_enabled?: boolean;
|
||||
aliyun_captcha_access_key_id?: string;
|
||||
aliyun_captcha_access_key_secret?: string;
|
||||
aliyun_captcha_scene_id?: string;
|
||||
aliyun_captcha_prefix?: string;
|
||||
aliyun_captcha_region?: string;
|
||||
api_key_acl_trust_forwarded_ip?: boolean;
|
||||
forwarded_client_ip_headers?: string[];
|
||||
linuxdo_connect_enabled?: boolean;
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
SendVerifyCodeRequest,
|
||||
SendVerifyCodeResponse,
|
||||
PublicSettings,
|
||||
TencentCaptchaRequestProof,
|
||||
ActionCaptchaRequestProof,
|
||||
TotpLoginResponse,
|
||||
TotpLogin2FARequest
|
||||
} from '@/types'
|
||||
@@ -51,7 +51,7 @@ export function buildOAuthLoginStartURL(request: OAuthLoginStart): string {
|
||||
|
||||
export async function startOAuthLogin(
|
||||
request: OAuthLoginStart,
|
||||
proof: TencentCaptchaRequestProof
|
||||
proof: ActionCaptchaRequestProof
|
||||
): Promise<OAuthLoginStartResponse> {
|
||||
const { data } = await apiClient.post<OAuthLoginStartResponse>(
|
||||
`/auth/oauth/${request.provider}/start`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { apiClient } from './client'
|
||||
import type { AuthResponse, TencentCaptchaRequestProof } from '@/types'
|
||||
import type { ActionCaptchaRequestProof, AuthResponse } from '@/types'
|
||||
|
||||
export interface PasskeyCredentialSummary {
|
||||
id: number
|
||||
@@ -104,7 +104,7 @@ function serializeAssertionCredential(credential: PublicKeyCredential): Record<s
|
||||
}
|
||||
}
|
||||
|
||||
async function login(proof?: TencentCaptchaRequestProof): Promise<AuthResponse> {
|
||||
async function login(proof?: ActionCaptchaRequestProof): Promise<AuthResponse> {
|
||||
requirePasskeySupport()
|
||||
const { data: begin } = proof
|
||||
? await apiClient.post<CeremonyOptionsResponse>('/auth/passkey/login/begin', proof)
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div v-if="sceneId && prefix" class="aliyun-captcha-wrapper">
|
||||
<button
|
||||
:id="buttonId"
|
||||
type="button"
|
||||
class="aliyun-captcha-button"
|
||||
:class="state === 'verified' ? 'aliyun-captcha-button--verified' : ''"
|
||||
:disabled="state === 'verified'"
|
||||
>
|
||||
<svg
|
||||
v-if="state === 'verified'"
|
||||
class="aliyun-captcha-icon"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="aliyun-captcha-icon"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9.661 2.237a.531.531 0 01.678 0 11.947 11.947 0 007.078 2.749.5.5 0 01.479.425c.069.52.104 1.05.104 1.59 0 5.162-3.26 9.563-7.834 11.256a.48.48 0 01-.332 0C5.26 16.564 2 12.163 2 7c0-.538.035-1.069.104-1.589a.5.5 0 01.48-.425 11.947 11.947 0 007.077-2.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ buttonText }}</span>
|
||||
</button>
|
||||
<div :id="elementId"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface AliyunCaptchaVerifyResult {
|
||||
captchaResult: boolean
|
||||
bizResult?: boolean
|
||||
}
|
||||
|
||||
interface AliyunCaptchaInitOptions {
|
||||
SceneId: string
|
||||
prefix: string
|
||||
mode: 'popup' | 'embed'
|
||||
element: string
|
||||
button: string
|
||||
captchaVerifyCallback: (
|
||||
captchaVerifyParam: string
|
||||
) => AliyunCaptchaVerifyResult | Promise<AliyunCaptchaVerifyResult>
|
||||
onBizResultCallback: (bizResult: boolean) => void
|
||||
getInstance: (instance: unknown) => void
|
||||
slideStyle?: { width: number; height: number }
|
||||
language?: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
initAliyunCaptcha?: (options: AliyunCaptchaInitOptions) => void
|
||||
AliyunCaptchaConfig?: { region: string; prefix: string }
|
||||
}
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
sceneId: string
|
||||
prefix: string
|
||||
region?: 'cn' | 'sgp'
|
||||
}>(),
|
||||
{
|
||||
region: 'cn'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'verify', param: string): void
|
||||
(e: 'expire'): void
|
||||
(e: 'error'): void
|
||||
}>()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const uid = Math.random().toString(36).slice(2, 10)
|
||||
const buttonId = `aliyun-captcha-button-${uid}`
|
||||
const elementId = `aliyun-captcha-element-${uid}`
|
||||
|
||||
// idle: 未验证可点击;verifying: 弹窗已拉起(关闭后回到 idle 可重试);verified: 已通过
|
||||
const state = ref<'idle' | 'verifying' | 'verified'>('idle')
|
||||
|
||||
const buttonText = computed(() => {
|
||||
switch (state.value) {
|
||||
case 'verified':
|
||||
return t('auth.captchaVerified')
|
||||
case 'verifying':
|
||||
return t('auth.captchaVerifying')
|
||||
default:
|
||||
return t('auth.captchaClickToVerify')
|
||||
}
|
||||
})
|
||||
|
||||
const SCRIPT_SRC = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js'
|
||||
const POPUP_ID = 'aliyunCaptcha-window-popup'
|
||||
const MASK_ID = 'aliyunCaptcha-mask'
|
||||
const POPUP_OPEN_TIMEOUT_MS = 8000
|
||||
const POPUP_WATCH_INTERVAL_MS = 300
|
||||
|
||||
// captchaVerifyParam 是一次性参数:verified 后缓存于此,提交失败需 reset 后重新验证
|
||||
let cachedParam: string | null = null
|
||||
let pending: { resolve: (value: string | null) => void } | null = null
|
||||
let popupWatchTimer: number | null = null
|
||||
let readyPromise: Promise<void> | null = null
|
||||
|
||||
const loadScript = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 全局配置必须在脚本加载前就位(region/prefix 全站一致,重复赋值无副作用)
|
||||
window.AliyunCaptchaConfig = { region: props.region, prefix: props.prefix }
|
||||
|
||||
if (window.initAliyunCaptcha) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||||
'script[src*="aliyunCaptcha/AliyunCaptcha"]'
|
||||
)
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener('load', () => resolve())
|
||||
existingScript.addEventListener('error', () =>
|
||||
reject(new Error('Failed to load Aliyun captcha script'))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = SCRIPT_SRC
|
||||
script.async = true
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error('Failed to load Aliyun captcha script'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
function initCaptcha(): void {
|
||||
if (!window.initAliyunCaptcha) {
|
||||
throw new Error('Aliyun captcha script not ready')
|
||||
}
|
||||
window.initAliyunCaptcha({
|
||||
SceneId: props.sceneId,
|
||||
prefix: props.prefix,
|
||||
mode: 'popup',
|
||||
element: `#${elementId}`,
|
||||
button: `#${buttonId}`,
|
||||
// 这里不发业务请求,只把 captchaVerifyParam 当 token 交给页面,随登录/注册等
|
||||
// 业务请求的 turnstile_token 字段提交,由后端在业务接口内调阿里云校验。
|
||||
captchaVerifyCallback: (captchaVerifyParam: string) => {
|
||||
onCaptchaParam(captchaVerifyParam)
|
||||
return { captchaResult: true }
|
||||
},
|
||||
onBizResultCallback: () => {},
|
||||
getInstance: () => {},
|
||||
slideStyle: { width: 360, height: 40 },
|
||||
language: locale.value.toLowerCase().startsWith('zh') ? 'cn' : 'en'
|
||||
})
|
||||
}
|
||||
|
||||
function ensureReady(): Promise<void> {
|
||||
if (!readyPromise) {
|
||||
readyPromise = loadScript().then(() => initCaptcha())
|
||||
readyPromise.catch(() => {
|
||||
// 失败后允许下次重试(如网络恢复)
|
||||
readyPromise = null
|
||||
})
|
||||
}
|
||||
return readyPromise
|
||||
}
|
||||
|
||||
function onCaptchaParam(param: string): void {
|
||||
stopPopupWatch()
|
||||
cachedParam = param
|
||||
state.value = 'verified'
|
||||
emit('verify', param)
|
||||
const current = pending
|
||||
pending = null
|
||||
current?.resolve(param)
|
||||
}
|
||||
|
||||
function settlePending(value: string | null): void {
|
||||
const current = pending
|
||||
pending = null
|
||||
current?.resolve(value)
|
||||
}
|
||||
|
||||
function isPopupVisible(): boolean {
|
||||
const popup = document.getElementById(POPUP_ID)
|
||||
if (!popup) return false
|
||||
return window.getComputedStyle(popup).display !== 'none'
|
||||
}
|
||||
|
||||
function stopPopupWatch(): void {
|
||||
if (popupWatchTimer !== null) {
|
||||
window.clearInterval(popupWatchTimer)
|
||||
popupWatchTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// SDK 没有用户关闭弹窗的回调,靠轮询弹窗可见性兜底:
|
||||
// 弹窗出现过又消失且未产生 param → 用户主动关闭;迟迟未出现 → 打开失败。
|
||||
// initAliyunCaptcha 对触发按钮的事件绑定是异步完成的,首次 click 可能落空,
|
||||
// 因此弹窗出现前每个 tick 重试触发一次。
|
||||
function startPopupWatch(): void {
|
||||
stopPopupWatch()
|
||||
const startedAt = Date.now()
|
||||
let seen = false
|
||||
popupWatchTimer = window.setInterval(() => {
|
||||
if (state.value === 'verified') {
|
||||
stopPopupWatch()
|
||||
return
|
||||
}
|
||||
if (isPopupVisible()) {
|
||||
seen = true
|
||||
return
|
||||
}
|
||||
if (seen || Date.now() - startedAt > POPUP_OPEN_TIMEOUT_MS) {
|
||||
stopPopupWatch()
|
||||
state.value = 'idle'
|
||||
settlePending(null)
|
||||
return
|
||||
}
|
||||
document.getElementById(buttonId)?.click()
|
||||
}, POPUP_WATCH_INTERVAL_MS)
|
||||
}
|
||||
|
||||
// 用户点击与程序化触发共用:置 verifying 并启动弹窗监视(幂等,重试 click 不重置计时)
|
||||
function handleTriggerClick(): void {
|
||||
if (state.value === 'verified') {
|
||||
return
|
||||
}
|
||||
state.value = 'verifying'
|
||||
if (popupWatchTimer === null) {
|
||||
startPopupWatch()
|
||||
}
|
||||
}
|
||||
|
||||
// 程序化触发验证(OAuth 启动、passkey、未预验证时的表单提交兜底):
|
||||
// 已通过预验证则直接复用缓存的 captchaVerifyParam;否则弹出验证码等待结果。
|
||||
// 用户关闭/未能弹出 resolve null;脚本加载失败 reject。
|
||||
async function verify(): Promise<string | null> {
|
||||
if (state.value === 'verified' && cachedParam) {
|
||||
return cachedParam
|
||||
}
|
||||
settlePending(null)
|
||||
await ensureReady()
|
||||
return new Promise<string | null>((resolve) => {
|
||||
pending = { resolve }
|
||||
document.getElementById(buttonId)?.click()
|
||||
})
|
||||
}
|
||||
|
||||
// 重置为未验证态;captchaVerifyParam 是一次性参数,服务端校验失败后需用户重新验证
|
||||
function reset(): void {
|
||||
stopPopupWatch()
|
||||
settlePending(null)
|
||||
cachedParam = null
|
||||
state.value = 'idle'
|
||||
}
|
||||
|
||||
defineExpose({ verify, reset })
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.sceneId || !props.prefix) {
|
||||
return
|
||||
}
|
||||
|
||||
document.getElementById(buttonId)?.addEventListener('click', handleTriggerClick)
|
||||
try {
|
||||
await ensureReady()
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Aliyun captcha:', error)
|
||||
emit('error')
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.getElementById(buttonId)?.removeEventListener('click', handleTriggerClick)
|
||||
stopPopupWatch()
|
||||
settlePending(null)
|
||||
// SDK 不会自清理弹窗 DOM,残留会导致下次挂载时回调重复触发
|
||||
document.getElementById(MASK_ID)?.remove()
|
||||
document.getElementById(POPUP_ID)?.remove()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aliyun-captcha-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aliyun-captcha-button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgb(209 213 219);
|
||||
background-color: rgb(249 250 251);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgb(55 65 81);
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.aliyun-captcha-button:hover:not(:disabled) {
|
||||
border-color: rgb(156 163 175);
|
||||
background-color: rgb(243 244 246);
|
||||
}
|
||||
|
||||
.aliyun-captcha-button--verified {
|
||||
border-color: rgb(34 197 94);
|
||||
background-color: rgb(240 253 244);
|
||||
color: rgb(21 128 61);
|
||||
}
|
||||
|
||||
:root.dark .aliyun-captcha-button,
|
||||
.dark .aliyun-captcha-button {
|
||||
border-color: rgb(55 65 81);
|
||||
background-color: rgb(31 41 55);
|
||||
color: rgb(209 213 219);
|
||||
}
|
||||
|
||||
:root.dark .aliyun-captcha-button:hover:not(:disabled),
|
||||
.dark .aliyun-captcha-button:hover:not(:disabled) {
|
||||
border-color: rgb(75 85 99);
|
||||
background-color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
:root.dark .aliyun-captcha-button--verified,
|
||||
.dark .aliyun-captcha-button--verified {
|
||||
border-color: rgb(34 197 94);
|
||||
background-color: rgb(20 83 45 / 0.3);
|
||||
color: rgb(134 239 172);
|
||||
}
|
||||
|
||||
.aliyun-captcha-icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -12,13 +12,30 @@
|
||||
ref="tencentRef"
|
||||
:app-id="tencentAppId"
|
||||
/>
|
||||
<AliyunCaptchaWidget
|
||||
v-else-if="aliyunEnabled && aliyunSceneId && aliyunPrefix"
|
||||
ref="aliyunRef"
|
||||
:scene-id="aliyunSceneId"
|
||||
:prefix="aliyunPrefix"
|
||||
:region="aliyunRegion === 'sgp' ? 'sgp' : 'cn'"
|
||||
@verify="(param: string) => emit('verify', param, '')"
|
||||
@expire="emit('expire')"
|
||||
@error="emit('error')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import TurnstileWidget from '@/components/TurnstileWidget.vue'
|
||||
import TencentCaptchaGate from '@/components/TencentCaptchaGate.vue'
|
||||
import type { TencentCaptchaProof } from '@/utils/tencentCaptcha'
|
||||
import AliyunCaptchaWidget from '@/components/AliyunCaptchaWidget.vue'
|
||||
|
||||
// ActionCaptchaResult 动作触发式验证(腾讯/阿里云弹窗)的结果:
|
||||
// 腾讯 token=ticket、randstr 非空;阿里云 token=captchaVerifyParam、randstr 恒为空。
|
||||
export interface ActionCaptchaResult {
|
||||
token: string
|
||||
randstr: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
siteKey?: string
|
||||
@@ -26,6 +43,10 @@ const props = defineProps<{
|
||||
turnstileSiteKey: string
|
||||
tencentEnabled: boolean
|
||||
tencentAppId: string
|
||||
aliyunEnabled?: boolean
|
||||
aliyunSceneId?: string
|
||||
aliyunPrefix?: string
|
||||
aliyunRegion?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -36,21 +57,39 @@ const emit = defineEmits<{
|
||||
|
||||
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
|
||||
const tencentRef = ref<InstanceType<typeof TencentCaptchaGate> | null>(null)
|
||||
const aliyunRef = ref<InstanceType<typeof AliyunCaptchaWidget> | null>(null)
|
||||
|
||||
function reset(): void {
|
||||
turnstileRef.value?.reset()
|
||||
tencentRef.value?.reset()
|
||||
aliyunRef.value?.reset()
|
||||
}
|
||||
|
||||
async function verifyTencent(): Promise<TencentCaptchaProof | null> {
|
||||
if (!props.tencentEnabled || !props.tencentAppId) return null
|
||||
try {
|
||||
return (await tencentRef.value?.verify()) ?? null
|
||||
} catch {
|
||||
emit('error')
|
||||
return null
|
||||
// verifyAction 弹出当前启用的动作触发式验证码(腾讯/阿里云)并等待结果;
|
||||
// 用户关闭弹窗返回 null,验证异常 emit('error') 并返回 null。
|
||||
async function verifyAction(): Promise<ActionCaptchaResult | null> {
|
||||
if (props.tencentEnabled && props.tencentAppId) {
|
||||
try {
|
||||
const proof = (await tencentRef.value?.verify()) ?? null
|
||||
if (!proof) return null
|
||||
return { token: proof.ticket, randstr: proof.randstr }
|
||||
} catch {
|
||||
emit('error')
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (props.aliyunEnabled && props.aliyunSceneId && props.aliyunPrefix) {
|
||||
try {
|
||||
const param = (await aliyunRef.value?.verify()) ?? null
|
||||
if (!param) return null
|
||||
return { token: param, randstr: '' }
|
||||
} catch {
|
||||
emit('error')
|
||||
return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
defineExpose({ reset, verifyTencent })
|
||||
defineExpose({ reset, verifyAction })
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import AliyunCaptchaWidget from '../AliyunCaptchaWidget.vue'
|
||||
|
||||
interface CapturedInitOptions {
|
||||
SceneId: string
|
||||
prefix: string
|
||||
mode: string
|
||||
element: string
|
||||
button: string
|
||||
captchaVerifyCallback: (param: string) => { captchaResult: boolean }
|
||||
language?: string
|
||||
}
|
||||
|
||||
const i18nStub = {
|
||||
install(app: { config: { globalProperties: Record<string, unknown> } }) {
|
||||
app.config.globalProperties.$t = (key: string) => key
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ locale: { value: 'zh-CN' }, t: (key: string) => key })
|
||||
}))
|
||||
|
||||
describe('AliyunCaptchaWidget', () => {
|
||||
let initOptions: CapturedInitOptions | null
|
||||
|
||||
beforeEach(() => {
|
||||
initOptions = null
|
||||
window.initAliyunCaptcha = vi.fn((options: CapturedInitOptions) => {
|
||||
initOptions = options
|
||||
}) as unknown as typeof window.initAliyunCaptcha
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
delete window.initAliyunCaptcha
|
||||
delete window.AliyunCaptchaConfig
|
||||
document.getElementById('aliyunCaptcha-window-popup')?.remove()
|
||||
document.getElementById('aliyunCaptcha-mask')?.remove()
|
||||
})
|
||||
|
||||
function mountWidget() {
|
||||
return mount(AliyunCaptchaWidget, {
|
||||
props: { sceneId: 'scene-1', prefix: 'prefix-1', region: 'cn' as const },
|
||||
attachTo: document.body,
|
||||
global: { plugins: [i18nStub] }
|
||||
})
|
||||
}
|
||||
|
||||
function createVisiblePopup(): HTMLElement {
|
||||
const popup = document.createElement('div')
|
||||
popup.id = 'aliyunCaptcha-window-popup'
|
||||
popup.style.display = 'block'
|
||||
document.body.appendChild(popup)
|
||||
return popup
|
||||
}
|
||||
|
||||
it('渲染可见验证按钮并以 popup 模式初始化,全局配置就位', async () => {
|
||||
const wrapper = mountWidget()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const button = wrapper.get('button')
|
||||
expect(button.text()).toContain('auth.captchaClickToVerify')
|
||||
expect(window.AliyunCaptchaConfig).toEqual({ region: 'cn', prefix: 'prefix-1' })
|
||||
expect(initOptions).not.toBeNull()
|
||||
expect(initOptions!.mode).toBe('popup')
|
||||
expect(initOptions!.SceneId).toBe('scene-1')
|
||||
expect(initOptions!.language).toBe('cn')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('用户点击按钮进入验证中,验证完成后 emit verify 并置已通过', async () => {
|
||||
const wrapper = mountWidget()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.get('button').text()).toContain('auth.captchaVerifying')
|
||||
|
||||
const result = initOptions!.captchaVerifyCallback('captcha-param-1')
|
||||
expect(result).toEqual({ captchaResult: true })
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted('verify')).toEqual([['captcha-param-1']])
|
||||
expect(wrapper.get('button').text()).toContain('auth.captchaVerified')
|
||||
expect(wrapper.get('button').attributes('disabled')).toBeDefined()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('verify() 在已预验证时直接复用缓存的 captchaVerifyParam', async () => {
|
||||
const wrapper = mountWidget()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
initOptions!.captchaVerifyCallback('captcha-param-2')
|
||||
|
||||
const vm = wrapper.vm as unknown as { verify: () => Promise<string | null> }
|
||||
await expect(vm.verify()).resolves.toBe('captcha-param-2')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('verify() 在未预验证时触发弹窗流程并等待结果', async () => {
|
||||
const wrapper = mountWidget()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const vm = wrapper.vm as unknown as { verify: () => Promise<string | null> }
|
||||
const pending = vm.verify()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.get('button').text()).toContain('auth.captchaVerifying')
|
||||
|
||||
initOptions!.captchaVerifyCallback('captcha-param-3')
|
||||
await expect(pending).resolves.toBe('captcha-param-3')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('弹窗未出现前会按 tick 重试触发按钮(SDK 异步绑定兜底)', async () => {
|
||||
vi.useFakeTimers()
|
||||
const wrapper = mountWidget()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const vm = wrapper.vm as unknown as { verify: () => Promise<string | null> }
|
||||
void vm.verify()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const button = wrapper.get('button').element as HTMLButtonElement
|
||||
const clickSpy = vi.fn()
|
||||
button.addEventListener('click', clickSpy)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
expect(clickSpy.mock.calls.length).toBeGreaterThanOrEqual(3)
|
||||
|
||||
button.removeEventListener('click', clickSpy)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('弹窗出现后被用户关闭时 resolve null 并回到未验证态', async () => {
|
||||
vi.useFakeTimers()
|
||||
const wrapper = mountWidget()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const vm = wrapper.vm as unknown as { verify: () => Promise<string | null> }
|
||||
const pending = vm.verify()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
const popup = createVisiblePopup()
|
||||
await vi.advanceTimersByTimeAsync(400)
|
||||
popup.remove()
|
||||
await vi.advanceTimersByTimeAsync(400)
|
||||
|
||||
await expect(pending).resolves.toBeNull()
|
||||
expect(wrapper.get('button').text()).toContain('auth.captchaClickToVerify')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('reset() 清空缓存并取消进行中的验证', async () => {
|
||||
const wrapper = mountWidget()
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
initOptions!.captchaVerifyCallback('captcha-param-4')
|
||||
|
||||
const vm = wrapper.vm as unknown as {
|
||||
verify: () => Promise<string | null>
|
||||
reset: () => void
|
||||
}
|
||||
vm.reset()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.get('button').text()).toContain('auth.captchaClickToVerify')
|
||||
|
||||
// reset 后缓存失效,verify() 重新走弹窗流程
|
||||
const pending = vm.verify()
|
||||
await Promise.resolve()
|
||||
initOptions!.captchaVerifyCallback('captcha-param-5')
|
||||
await expect(pending).resolves.toBe('captcha-param-5')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
initAliyunCaptcha?: (options: unknown) => void
|
||||
AliyunCaptchaConfig?: { region: string; prefix: string }
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
:aliyun-region="aliyunCaptchaRegion"
|
||||
@verify="onTurnstileVerify"
|
||||
@expire="onTurnstileExpire"
|
||||
@error="onTurnstileError"
|
||||
@@ -137,13 +141,28 @@ const turnstileEnabled = ref(false)
|
||||
const turnstileSiteKey = ref('')
|
||||
const tencentCaptchaEnabled = ref(false)
|
||||
const tencentCaptchaAppId = ref('')
|
||||
const aliyunCaptchaEnabled = ref(false)
|
||||
const aliyunCaptchaSceneId = ref('')
|
||||
const aliyunCaptchaPrefix = ref('')
|
||||
const aliyunCaptchaRegion = ref('cn')
|
||||
const turnstileToken = ref('')
|
||||
const tencentCaptchaRandstr = ref('')
|
||||
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
|
||||
const aliyunCaptchaReady = computed(
|
||||
() =>
|
||||
aliyunCaptchaEnabled.value &&
|
||||
Boolean(aliyunCaptchaSceneId.value) &&
|
||||
Boolean(aliyunCaptchaPrefix.value)
|
||||
)
|
||||
// 动作触发式验证码(腾讯/阿里云):发送验证码、提交时弹窗验证
|
||||
const actionCaptchaEnabled = computed(
|
||||
() =>
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value)) ||
|
||||
aliyunCaptchaReady.value
|
||||
)
|
||||
const captchaEnabled = computed(
|
||||
() =>
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) ||
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value))
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) || actionCaptchaEnabled.value
|
||||
)
|
||||
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
@@ -229,13 +248,13 @@ function onTurnstileError() {
|
||||
sendCodeError.value = t('auth.turnstileFailed')
|
||||
}
|
||||
|
||||
async function acquireTencentProof(): Promise<boolean> {
|
||||
if (!tencentCaptchaEnabled.value) return true
|
||||
async function acquireActionProof(): Promise<boolean> {
|
||||
if (!actionCaptchaEnabled.value) return true
|
||||
|
||||
const proof = await turnstileRef.value?.verifyTencent()
|
||||
const proof = await turnstileRef.value?.verifyAction()
|
||||
if (!proof) return false
|
||||
|
||||
turnstileToken.value = proof.ticket
|
||||
turnstileToken.value = proof.token
|
||||
tencentCaptchaRandstr.value = proof.randstr
|
||||
return true
|
||||
}
|
||||
@@ -251,7 +270,7 @@ async function handleSendCode() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireTencentProof())) {
|
||||
if (!(await acquireActionProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -262,7 +281,8 @@ async function handleSendCode() {
|
||||
try {
|
||||
const response = await sendPendingOAuthVerifyCode({
|
||||
email: trimmedEmail,
|
||||
turnstile_token: turnstileEnabled.value ? turnstileToken.value : undefined,
|
||||
turnstile_token:
|
||||
turnstileEnabled.value || aliyunCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_ticket: tencentCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_randstr: tencentCaptchaEnabled.value ? tencentCaptchaRandstr.value : undefined
|
||||
})
|
||||
@@ -292,7 +312,7 @@ async function handleSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireTencentProof())) {
|
||||
if (!(await acquireActionProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,7 +320,9 @@ async function handleSubmit() {
|
||||
email: trimmedEmail,
|
||||
password: password.value,
|
||||
verifyCode: emailVerifyEnabled.value ? verifyCode.value.trim() : '',
|
||||
...(turnstileEnabled.value && turnstileToken.value ? { turnstileToken: turnstileToken.value } : {}),
|
||||
...((turnstileEnabled.value || aliyunCaptchaEnabled.value) && turnstileToken.value
|
||||
? { turnstileToken: turnstileToken.value }
|
||||
: {}),
|
||||
...(tencentCaptchaEnabled.value && turnstileToken.value
|
||||
? {
|
||||
tencentCaptchaTicket: turnstileToken.value,
|
||||
@@ -310,7 +332,7 @@ async function handleSubmit() {
|
||||
invitationCode: invitationCode.value.trim() || undefined
|
||||
})
|
||||
|
||||
if (tencentCaptchaEnabled.value) {
|
||||
if (actionCaptchaEnabled.value) {
|
||||
resetTurnstile()
|
||||
}
|
||||
}
|
||||
@@ -328,6 +350,10 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
aliyunCaptchaRegion.value = settings.aliyun_captcha_region || 'cn'
|
||||
} catch {
|
||||
invitationCodeEnabled.value = false
|
||||
emailVerifyEnabled.value = true
|
||||
@@ -335,6 +361,10 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = ''
|
||||
tencentCaptchaEnabled.value = false
|
||||
tencentCaptchaAppId.value = ''
|
||||
aliyunCaptchaEnabled.value = false
|
||||
aliyunCaptchaSceneId.value = ''
|
||||
aliyunCaptchaPrefix.value = ''
|
||||
aliyunCaptchaRegion.value = 'cn'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const sendPendingOAuthVerifyCode = vi.fn()
|
||||
const getPublicSettings = vi.fn()
|
||||
const showError = vi.fn()
|
||||
const turnstileReset = vi.fn()
|
||||
const verifyTencent = vi.fn()
|
||||
const verifyAction = vi.fn()
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
@@ -44,7 +44,7 @@ describe('PendingOAuthCreateAccountForm', () => {
|
||||
getPublicSettings.mockReset()
|
||||
showError.mockReset()
|
||||
turnstileReset.mockReset()
|
||||
verifyTencent.mockReset()
|
||||
verifyAction.mockReset()
|
||||
getPublicSettings.mockResolvedValue({
|
||||
turnstile_enabled: false,
|
||||
turnstile_site_key: ''
|
||||
@@ -60,12 +60,12 @@ describe('PendingOAuthCreateAccountForm', () => {
|
||||
tencent_captcha_app_id: 'tencent-app-id'
|
||||
})
|
||||
sendPendingOAuthVerifyCode.mockResolvedValue({ countdown: 0 })
|
||||
verifyTencent
|
||||
.mockResolvedValueOnce({ ticket: 'ticket-1', randstr: '@rand-1' })
|
||||
.mockResolvedValueOnce({ ticket: 'ticket-2', randstr: '@rand-2' })
|
||||
verifyAction
|
||||
.mockResolvedValueOnce({ token: 'ticket-1', randstr: '@rand-1' })
|
||||
.mockResolvedValueOnce({ token: 'ticket-2', randstr: '@rand-2' })
|
||||
const CaptchaChallengeStub = defineComponent({
|
||||
setup(_, { expose }) {
|
||||
expose({ verifyTencent, reset: turnstileReset })
|
||||
expose({ verifyAction, reset: turnstileReset })
|
||||
return () => h('div')
|
||||
}
|
||||
})
|
||||
@@ -89,7 +89,7 @@ describe('PendingOAuthCreateAccountForm', () => {
|
||||
await wrapper.get('[data-testid="oidc-create-account-submit"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyTencent).toHaveBeenCalledTimes(2)
|
||||
expect(verifyAction).toHaveBeenCalledTimes(2)
|
||||
expect(sendPendingOAuthVerifyCode).toHaveBeenCalledWith({
|
||||
email: 'user@example.com',
|
||||
tencent_captcha_ticket: 'ticket-1',
|
||||
|
||||
@@ -183,6 +183,16 @@ export default {
|
||||
secretKeyHint: 'Server-side verification key (keep this secret)',
|
||||
secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.'
|
||||
},
|
||||
captcha: {
|
||||
title: 'CAPTCHA',
|
||||
description: 'Bot protection for login and registration',
|
||||
enable: 'Enable CAPTCHA',
|
||||
enableHint: 'Require human verification on login, registration and related flows',
|
||||
provider: 'Provider',
|
||||
providerTurnstile: 'Cloudflare Turnstile',
|
||||
providerTencent: 'Tencent Captcha',
|
||||
providerAliyun: 'Aliyun Captcha 2.0'
|
||||
},
|
||||
tencentCaptcha: {
|
||||
title: 'Tencent Captcha',
|
||||
description: 'Slider captcha protection for login, registration, and third-party account creation',
|
||||
@@ -191,7 +201,7 @@ export default {
|
||||
keepExisting: 'Leave empty to keep current value',
|
||||
configured: 'Configured. Leave empty to keep it.',
|
||||
required: 'Required before enabling.',
|
||||
mutualExclusion: 'Tencent Captcha and Cloudflare Turnstile are mutually exclusive. Enabling one disables the other.',
|
||||
mutualExclusion: 'Tencent Captcha, Cloudflare Turnstile and Aliyun Captcha are mutually exclusive. Enabling one disables the others.',
|
||||
appCredentialsTitle: 'Captcha application credentials',
|
||||
appCredentialsHint: 'Get CaptchaAppId and AppSecretKey from Verification Management in the Captcha console.',
|
||||
cloudCredentialsTitle: 'Cloud API credentials',
|
||||
@@ -206,6 +216,21 @@ export default {
|
||||
createCloudKeys: 'Create SecretId / SecretKey',
|
||||
openWebDocs: 'View Web integration guide'
|
||||
},
|
||||
aliyunCaptcha: {
|
||||
accessKeyId: 'AccessKey ID',
|
||||
accessKeyIdHint: 'Alibaba Cloud AccessKey ID used for server-side verification; a captcha-only RAM user is recommended',
|
||||
accessKeySecret: 'AccessKey Secret',
|
||||
accessKeySecretHint: 'Server-side verification secret (keep this secret)',
|
||||
accessKeySecretConfiguredHint: 'Secret configured. Leave empty to keep the current value.',
|
||||
sceneId: 'Scene ID',
|
||||
sceneIdHint: 'Create a verification scene in the Alibaba Cloud Captcha console; the captcha type (invisible/slider/puzzle) is configured per scene there',
|
||||
prefix: 'Prefix',
|
||||
prefixHint: 'Found in the instance information on the console overview page',
|
||||
region: 'Region',
|
||||
regionCn: 'Mainland China',
|
||||
regionSgp: 'Singapore',
|
||||
regionHint: 'Determines the frontend script region and the server endpoint; must match your captcha instance region'
|
||||
},
|
||||
apiKeyAcl: {
|
||||
title: 'API Key IP Access Control',
|
||||
description:
|
||||
|
||||
@@ -245,6 +245,8 @@ export default {
|
||||
turnstileFailed: 'Verification failed, please try again',
|
||||
captchaVerified: 'Verification completed',
|
||||
captchaLoading: 'Loading verification…',
|
||||
captchaClickToVerify: 'Click to complete verification',
|
||||
captchaVerifying: 'Verifying…',
|
||||
completeVerification: 'Please complete the verification',
|
||||
verifyYourEmail: 'Verify Your Email',
|
||||
sessionExpired: 'Session expired',
|
||||
|
||||
@@ -183,6 +183,16 @@ export default {
|
||||
secretKeyHint: '服务端验证密钥(请保密)',
|
||||
secretKeyConfiguredHint: '密钥已配置,留空以保留当前值。'
|
||||
},
|
||||
captcha: {
|
||||
title: '人机验证',
|
||||
description: '登录和注册的机器人防护',
|
||||
enable: '启用人机验证',
|
||||
enableHint: '开启后登录、注册等入口需要通过人机验证',
|
||||
provider: '验证服务商',
|
||||
providerTurnstile: 'Cloudflare Turnstile',
|
||||
providerTencent: '腾讯天御验证码',
|
||||
providerAliyun: '阿里云验证码 2.0'
|
||||
},
|
||||
tencentCaptcha: {
|
||||
title: '腾讯天御验证码',
|
||||
description: '为登录、注册及第三方登录创建账号流程提供滑动验证码保护',
|
||||
@@ -191,7 +201,7 @@ export default {
|
||||
keepExisting: '留空以保留当前值',
|
||||
configured: '已配置,留空不会覆盖。',
|
||||
required: '启用前必须填写此项。',
|
||||
mutualExclusion: '腾讯天御验证码与 Cloudflare Turnstile 互斥,开启其中一个会自动关闭另一个。',
|
||||
mutualExclusion: '腾讯天御验证码与 Cloudflare Turnstile、阿里云验证码互斥,开启其中一个会自动关闭其它。',
|
||||
appCredentialsTitle: '验证码应用密钥',
|
||||
appCredentialsHint: 'CaptchaAppId 与 AppSecretKey 来自验证码控制台的验证管理页面。',
|
||||
cloudCredentialsTitle: '云 API 调用密钥',
|
||||
@@ -206,6 +216,21 @@ export default {
|
||||
createCloudKeys: '创建 SecretId / SecretKey',
|
||||
openWebDocs: '查看 Web 接入文档'
|
||||
},
|
||||
aliyunCaptcha: {
|
||||
accessKeyId: 'AccessKey ID',
|
||||
accessKeyIdHint: '用于服务端验证的阿里云 AccessKey ID,建议使用仅含验证码权限的 RAM 子账号',
|
||||
accessKeySecret: 'AccessKey Secret',
|
||||
accessKeySecretHint: '服务端验证密钥(请保密)',
|
||||
accessKeySecretConfiguredHint: '密钥已配置,留空以保留当前值。',
|
||||
sceneId: '场景 ID',
|
||||
sceneIdHint: '在阿里云验证码控制台创建验证场景后获取;验证方式(无痕/滑块/拼图)在控制台按场景配置',
|
||||
prefix: '身份标(prefix)',
|
||||
prefixHint: '在控制台概览页实例基本信息中获取',
|
||||
region: '服务地域',
|
||||
regionCn: '中国内地',
|
||||
regionSgp: '新加坡',
|
||||
regionHint: '决定前端脚本接入区域与服务端接入点,需与阿里云验证码实例所属地域一致'
|
||||
},
|
||||
apiKeyAcl: {
|
||||
title: 'API Key IP 访问控制',
|
||||
description: '控制 API Key 白/黑名单、操作审计日志与会话 IP/UA 绑定使用哪个客户端 IP 判断',
|
||||
|
||||
@@ -244,6 +244,8 @@ export default {
|
||||
turnstileFailed: '验证失败,请重试',
|
||||
captchaVerified: '验证已完成',
|
||||
captchaLoading: '正在加载验证码…',
|
||||
captchaClickToVerify: '点击完成人机验证',
|
||||
captchaVerifying: '验证中…',
|
||||
completeVerification: '请完成验证',
|
||||
verifyYourEmail: '验证您的邮箱',
|
||||
sessionExpired: '会话已过期',
|
||||
|
||||
@@ -335,6 +335,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
invitation_code_enabled: false,
|
||||
turnstile_enabled: false,
|
||||
turnstile_site_key: '',
|
||||
aliyun_captcha_enabled: false,
|
||||
aliyun_captcha_scene_id: '',
|
||||
aliyun_captcha_prefix: '',
|
||||
aliyun_captcha_region: 'cn',
|
||||
site_name: siteName.value,
|
||||
site_logo: siteLogo.value,
|
||||
site_subtitle: '',
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
AuthResponse,
|
||||
TencentCaptchaRequestProof
|
||||
ActionCaptchaRequestProof
|
||||
} from '@/types'
|
||||
|
||||
const AUTH_TOKEN_KEY = 'auth_token'
|
||||
@@ -281,7 +281,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWithPasskey(proof?: TencentCaptchaRequestProof): Promise<User> {
|
||||
async function loginWithPasskey(proof?: ActionCaptchaRequestProof): Promise<User> {
|
||||
try {
|
||||
const response = await passkeyAPI.login(proof)
|
||||
setAuthFromResponse(response)
|
||||
|
||||
@@ -124,6 +124,12 @@ export interface TencentCaptchaRequestProof {
|
||||
tencent_captcha_randstr: string
|
||||
}
|
||||
|
||||
// 动作触发式验证码(OAuth 启动、passkey 等入口)的请求凭据:
|
||||
// 腾讯填 tencent_captcha_*,阿里云的 captchaVerifyParam 复用 turnstile_token 字段
|
||||
export interface ActionCaptchaRequestProof extends Partial<TencentCaptchaRequestProof> {
|
||||
turnstile_token?: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string
|
||||
password: string
|
||||
@@ -216,6 +222,10 @@ export interface PublicSettings {
|
||||
tencent_captcha_app_id?: string
|
||||
passkey_enabled?: boolean
|
||||
turnstile_site_key: string
|
||||
aliyun_captcha_enabled?: boolean
|
||||
aliyun_captcha_scene_id?: string
|
||||
aliyun_captcha_prefix?: string
|
||||
aliyun_captcha_region?: string
|
||||
site_name: string
|
||||
site_logo: string
|
||||
site_subtitle: string
|
||||
|
||||
@@ -1967,42 +1967,97 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare Turnstile Settings -->
|
||||
<!-- 人机验证 Settings -->
|
||||
<div class="card">
|
||||
<div
|
||||
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
||||
>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.turnstile.title") }}
|
||||
{{ t("admin.settings.captcha.title") }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.turnstile.description") }}
|
||||
{{ t("admin.settings.captcha.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-5 p-6">
|
||||
<!-- Enable Turnstile -->
|
||||
<!-- Enable Captcha -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">{{
|
||||
t("admin.settings.turnstile.enableTurnstile")
|
||||
t("admin.settings.captcha.enable")
|
||||
}}</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.turnstile.enableTurnstileHint") }}
|
||||
{{ t("admin.settings.captcha.enableHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
v-model="form.turnstile_enabled"
|
||||
data-testid="turnstile-enabled-toggle"
|
||||
@update:model-value="onTurnstileToggle"
|
||||
v-model="captchaMasterEnabled"
|
||||
data-testid="captcha-enabled-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Turnstile Keys - Only show when enabled -->
|
||||
<!-- Provider fields - Only show when enabled -->
|
||||
<div
|
||||
v-if="form.turnstile_enabled"
|
||||
v-if="captchaMasterEnabled"
|
||||
class="border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<!-- Provider Selector -->
|
||||
<div class="mb-6">
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.captcha.provider") }}
|
||||
</label>
|
||||
<div
|
||||
class="grid grid-cols-3 gap-2 rounded-lg bg-gray-100 p-1 dark:bg-dark-700"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="captcha-provider-turnstile"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
|
||||
:class="
|
||||
captchaProviderSelection === 'turnstile'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="selectCaptchaProvider('turnstile')"
|
||||
>
|
||||
{{ t("admin.settings.captcha.providerTurnstile") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="captcha-provider-tencent"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
|
||||
:class="
|
||||
captchaProviderSelection === 'tencent'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="selectCaptchaProvider('tencent')"
|
||||
>
|
||||
{{ t("admin.settings.captcha.providerTencent") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="captcha-provider-aliyun"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition"
|
||||
:class="
|
||||
captchaProviderSelection === 'aliyun'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="selectCaptchaProvider('aliyun')"
|
||||
>
|
||||
{{ t("admin.settings.captcha.providerAliyun") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare Turnstile fields -->
|
||||
<div
|
||||
v-if="captchaProviderSelection === 'turnstile'"
|
||||
class="grid grid-cols-1 gap-6"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
@@ -2050,151 +2105,232 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 腾讯天御验证码设置 -->
|
||||
<div class="card">
|
||||
<div class="border-b border-gray-100 px-6 py-4 dark:border-dark-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.tencentCaptcha.title") }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-5 p-6">
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<div>
|
||||
<label class="font-medium text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.tencentCaptcha.enable") }}
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.enableHint") }}
|
||||
<!-- Tencent Captcha fields -->
|
||||
<div v-else-if="captchaProviderSelection === 'tencent'">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="md:col-span-2">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.tencentCaptcha.appCredentialsTitle") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.appCredentialsHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.appId") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_app_id"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="input font-mono text-sm"
|
||||
placeholder="123456789"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.appSecretKey") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_app_secret_key"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ form.tencent_captcha_app_secret_key_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-100 pt-5 md:col-span-2 dark:border-dark-700">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudCredentialsTitle") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudCredentialsHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudSecretId") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_cloud_secret_id"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ form.tencent_captcha_cloud_secret_id_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudSecretKey") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_cloud_secret_key"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ form.tencent_captcha_cloud_secret_key_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.camPermissionHint") }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.aidEncryptedHint") }}
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-sm">
|
||||
<a
|
||||
href="https://console.cloud.tencent.com/captcha"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.openCaptchaConsole") }}
|
||||
</a>
|
||||
<a
|
||||
href="https://console.cloud.tencent.com/cam/capi"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.createCloudKeys") }}
|
||||
</a>
|
||||
<a
|
||||
href="https://cloud.tencent.com/document/product/1110/36841"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.openWebDocs") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Toggle
|
||||
v-model="form.tencent_captcha_enabled"
|
||||
data-testid="tencent-captcha-enabled-toggle"
|
||||
@update:model-value="onTencentCaptchaToggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.tencent_captcha_enabled"
|
||||
class="border-t border-gray-100 pt-4 dark:border-dark-700"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="md:col-span-2">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.tencentCaptcha.appCredentialsTitle") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.appCredentialsHint") }}
|
||||
</p>
|
||||
<!-- Aliyun Captcha 2.0 fields -->
|
||||
<div v-else class="grid grid-cols-1 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.region") }}
|
||||
</label>
|
||||
<div
|
||||
class="grid grid-cols-2 gap-2 rounded-lg bg-gray-100 p-1 dark:bg-dark-700"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="
|
||||
form.aliyun_captcha_region !== 'sgp'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="form.aliyun_captcha_region = 'cn'"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.regionCn") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition"
|
||||
:class="
|
||||
form.aliyun_captcha_region === 'sgp'
|
||||
? 'bg-white text-primary-700 shadow-sm dark:bg-dark-800 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-dark-300 dark:hover:text-white'
|
||||
"
|
||||
@click="form.aliyun_captcha_region = 'sgp'"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.regionSgp") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.aliyunCaptcha.regionHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.prefix") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.aliyun_captcha_prefix"
|
||||
type="text"
|
||||
class="input font-mono text-sm"
|
||||
placeholder="14xxxxx"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.aliyunCaptcha.prefixHint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.appId") }}
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.sceneId") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_app_id"
|
||||
v-model="form.aliyun_captcha_scene_id"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="input font-mono text-sm"
|
||||
placeholder="123456789"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.appSecretKey") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_app_secret_key"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
placeholder="1cxxxxxx"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ form.tencent_captcha_app_secret_key_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-100 pt-5 md:col-span-2 dark:border-dark-700">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudCredentialsTitle") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudCredentialsHint") }}
|
||||
{{ t("admin.settings.aliyunCaptcha.sceneIdHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudSecretId") }}
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.accessKeyId") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_cloud_secret_id"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
v-model="form.aliyun_captcha_access_key_id"
|
||||
type="text"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
placeholder="LTAI..."
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ form.tencent_captcha_cloud_secret_id_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
|
||||
{{ t("admin.settings.aliyunCaptcha.accessKeyIdHint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t("admin.settings.tencentCaptcha.cloudSecretKey") }}
|
||||
<label
|
||||
class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.aliyunCaptcha.accessKeySecret") }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.tencent_captcha_cloud_secret_key"
|
||||
v-model="form.aliyun_captcha_access_key_secret"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
class="input font-mono text-sm"
|
||||
:placeholder="t('admin.settings.tencentCaptcha.keepExisting')"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ form.tencent_captcha_cloud_secret_key_configured ? t("admin.settings.tencentCaptcha.configured") : t("admin.settings.tencentCaptcha.required") }}
|
||||
{{
|
||||
form.aliyun_captcha_access_key_secret_configured
|
||||
? t(
|
||||
"admin.settings.aliyunCaptcha.accessKeySecretConfiguredHint",
|
||||
)
|
||||
: t("admin.settings.aliyunCaptcha.accessKeySecretHint")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-5 text-xs text-amber-600 dark:text-amber-400">
|
||||
{{ t("admin.settings.tencentCaptcha.mutualExclusion") }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.camPermissionHint") }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.tencentCaptcha.aidEncryptedHint") }}
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-sm">
|
||||
<a
|
||||
href="https://console.cloud.tencent.com/captcha"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.openCaptchaConsole") }}
|
||||
</a>
|
||||
<a
|
||||
href="https://console.cloud.tencent.com/cam/capi"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.createCloudKeys") }}
|
||||
</a>
|
||||
<a
|
||||
href="https://cloud.tencent.com/document/product/1110/36841"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
{{ t("admin.settings.tencentCaptcha.openWebDocs") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8990,6 +9126,7 @@ type SettingsForm = Omit<
|
||||
tencent_captcha_app_secret_key: string;
|
||||
tencent_captcha_cloud_secret_id: string;
|
||||
tencent_captcha_cloud_secret_key: string;
|
||||
aliyun_captcha_access_key_secret: string;
|
||||
linuxdo_connect_client_secret: string;
|
||||
dingtalk_connect_client_secret: string;
|
||||
wechat_connect_app_secret: string;
|
||||
@@ -9127,6 +9264,13 @@ const form = reactive<SettingsForm>({
|
||||
tencent_captcha_cloud_secret_id_configured: false,
|
||||
tencent_captcha_cloud_secret_key: "",
|
||||
tencent_captcha_cloud_secret_key_configured: false,
|
||||
aliyun_captcha_enabled: false,
|
||||
aliyun_captcha_access_key_id: "",
|
||||
aliyun_captcha_access_key_secret: "",
|
||||
aliyun_captcha_access_key_secret_configured: false,
|
||||
aliyun_captcha_scene_id: "",
|
||||
aliyun_captcha_prefix: "",
|
||||
aliyun_captcha_region: "cn",
|
||||
api_key_acl_trust_forwarded_ip: true,
|
||||
forwarded_client_ip_headers: [],
|
||||
// LinuxDo Connect OAuth 登录
|
||||
@@ -9290,12 +9434,40 @@ const form = reactive<SettingsForm>({
|
||||
allow_user_view_error_requests: false,
|
||||
});
|
||||
|
||||
function onTurnstileToggle(enabled: boolean): void {
|
||||
if (enabled) form.tencent_captcha_enabled = false;
|
||||
// 人机验证 UI 状态:单卡片「总开关 + 服务商单选」,落库仍是三个独立
|
||||
// enabled 键(与上游一致),由下面的映射保证同一时间至多一家启用。
|
||||
type CaptchaProviderSelection = "turnstile" | "tencent" | "aliyun";
|
||||
|
||||
const captchaProviderSelection = ref<CaptchaProviderSelection>("turnstile");
|
||||
|
||||
function applyCaptchaSelection(provider: CaptchaProviderSelection | null): void {
|
||||
form.turnstile_enabled = provider === "turnstile";
|
||||
form.tencent_captcha_enabled = provider === "tencent";
|
||||
form.aliyun_captcha_enabled = provider === "aliyun";
|
||||
}
|
||||
|
||||
function onTencentCaptchaToggle(enabled: boolean): void {
|
||||
if (enabled) form.turnstile_enabled = false;
|
||||
const captchaMasterEnabled = computed({
|
||||
get: () =>
|
||||
form.turnstile_enabled ||
|
||||
form.tencent_captcha_enabled ||
|
||||
form.aliyun_captcha_enabled,
|
||||
set: (enabled: boolean) =>
|
||||
applyCaptchaSelection(enabled ? captchaProviderSelection.value : null),
|
||||
});
|
||||
|
||||
function selectCaptchaProvider(provider: CaptchaProviderSelection): void {
|
||||
captchaProviderSelection.value = provider;
|
||||
applyCaptchaSelection(provider);
|
||||
}
|
||||
|
||||
function syncCaptchaProviderSelection(): void {
|
||||
if (form.tencent_captcha_enabled) {
|
||||
captchaProviderSelection.value = "tencent";
|
||||
} else if (form.aliyun_captcha_enabled) {
|
||||
captchaProviderSelection.value = "aliyun";
|
||||
} else if (form.turnstile_enabled) {
|
||||
captchaProviderSelection.value = "turnstile";
|
||||
}
|
||||
}
|
||||
|
||||
type OpenAIAdvancedSchedulerOverrideKey =
|
||||
@@ -10209,6 +10381,7 @@ async function loadSettings() {
|
||||
(form as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
syncCaptchaProviderSelection();
|
||||
if (!form.claude_oauth_system_prompt_blocks?.trim()) {
|
||||
form.claude_oauth_system_prompt_blocks =
|
||||
defaultClaudeOAuthSystemPromptBlocks;
|
||||
@@ -10266,6 +10439,7 @@ async function loadSettings() {
|
||||
form.tencent_captcha_app_secret_key = "";
|
||||
form.tencent_captcha_cloud_secret_id = "";
|
||||
form.tencent_captcha_cloud_secret_key = "";
|
||||
form.aliyun_captcha_access_key_secret = "";
|
||||
form.linuxdo_connect_client_secret = "";
|
||||
form.dingtalk_connect_client_secret = "";
|
||||
form.github_oauth_client_secret = "";
|
||||
@@ -10643,6 +10817,13 @@ async function saveSettings() {
|
||||
form.tencent_captcha_cloud_secret_id || undefined,
|
||||
tencent_captcha_cloud_secret_key:
|
||||
form.tencent_captcha_cloud_secret_key || undefined,
|
||||
aliyun_captcha_enabled: form.aliyun_captcha_enabled,
|
||||
aliyun_captcha_access_key_id: form.aliyun_captcha_access_key_id,
|
||||
aliyun_captcha_access_key_secret:
|
||||
form.aliyun_captcha_access_key_secret || undefined,
|
||||
aliyun_captcha_scene_id: form.aliyun_captcha_scene_id,
|
||||
aliyun_captcha_prefix: form.aliyun_captcha_prefix,
|
||||
aliyun_captcha_region: form.aliyun_captcha_region,
|
||||
api_key_acl_trust_forwarded_ip: form.api_key_acl_trust_forwarded_ip,
|
||||
forwarded_client_ip_headers: form.forwarded_client_ip_headers,
|
||||
linuxdo_connect_enabled: form.linuxdo_connect_enabled,
|
||||
@@ -10933,6 +11114,7 @@ async function saveSettings() {
|
||||
form.smtp_password = "";
|
||||
smtpPasswordManuallyEdited.value = false;
|
||||
form.turnstile_secret_key = "";
|
||||
form.aliyun_captcha_access_key_secret = "";
|
||||
form.linuxdo_connect_client_secret = "";
|
||||
form.dingtalk_connect_client_secret = "";
|
||||
form.github_oauth_client_secret = "";
|
||||
|
||||
@@ -772,24 +772,24 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("腾讯天御验证码与 Turnstile 开关互斥并保存四项配置", async () => {
|
||||
it("人机验证切换到腾讯天御并保存四项配置", async () => {
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
const turnstileToggle = wrapper.get('[data-testid="turnstile-enabled-toggle"]');
|
||||
const tencentToggle = wrapper.get('[data-testid="tencent-captcha-enabled-toggle"]');
|
||||
await turnstileToggle.setValue(true);
|
||||
expect((turnstileToggle.element as HTMLInputElement).checked).toBe(true);
|
||||
const masterToggle = wrapper.get('[data-testid="captcha-enabled-toggle"]');
|
||||
await masterToggle.setValue(true);
|
||||
// 默认选中 Turnstile
|
||||
expect(wrapper.text()).toContain("admin.settings.turnstile.siteKey");
|
||||
|
||||
await tencentToggle.setValue(true);
|
||||
expect((turnstileToggle.element as HTMLInputElement).checked).toBe(false);
|
||||
expect((tencentToggle.element as HTMLInputElement).checked).toBe(true);
|
||||
await wrapper.get('[data-testid="captcha-provider-tencent"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
const card = wrapper
|
||||
.findAll(".card")
|
||||
.find((node) => node.text().includes("admin.settings.tencentCaptcha.title"));
|
||||
.find((node) => node.text().includes("admin.settings.captcha.title"));
|
||||
expect(card).toBeDefined();
|
||||
expect(card!.text()).not.toContain("admin.settings.turnstile.siteKey");
|
||||
expect(card!.get('a[href="https://console.cloud.tencent.com/captcha"]').exists()).toBe(true);
|
||||
expect(card!.get('a[href="https://console.cloud.tencent.com/cam/capi"]').exists()).toBe(true);
|
||||
expect(
|
||||
@@ -808,6 +808,7 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
expect.objectContaining({
|
||||
turnstile_enabled: false,
|
||||
tencent_captcha_enabled: true,
|
||||
aliyun_captcha_enabled: false,
|
||||
tencent_captcha_app_id: "123456789",
|
||||
tencent_captcha_app_secret_key: "app-secret-value",
|
||||
tencent_captcha_cloud_secret_id: "cloud-secret-id-value",
|
||||
@@ -816,6 +817,77 @@ describe("admin SettingsView payment visible method controls", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("人机验证切换到阿里云并保存配置", async () => {
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
const masterToggle = wrapper.get('[data-testid="captcha-enabled-toggle"]');
|
||||
await masterToggle.setValue(true);
|
||||
|
||||
await wrapper.get('[data-testid="captcha-provider-aliyun"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
const card = wrapper
|
||||
.findAll(".card")
|
||||
.find((node) => node.text().includes("admin.settings.captcha.title"));
|
||||
expect(card).toBeDefined();
|
||||
expect(card!.text()).toContain("admin.settings.aliyunCaptcha.region");
|
||||
expect(card!.text()).not.toContain("admin.settings.turnstile.siteKey");
|
||||
const inputs = card!.findAll("input").filter((input) => input.attributes("type") !== "checkbox");
|
||||
await inputs[0]!.setValue("prefix-1");
|
||||
await inputs[1]!.setValue("scene-1");
|
||||
await inputs[2]!.setValue("ak-id");
|
||||
await inputs[3]!.setValue("ak-secret-value");
|
||||
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
turnstile_enabled: false,
|
||||
tencent_captcha_enabled: false,
|
||||
aliyun_captcha_enabled: true,
|
||||
aliyun_captcha_prefix: "prefix-1",
|
||||
aliyun_captcha_scene_id: "scene-1",
|
||||
aliyun_captcha_access_key_id: "ak-id",
|
||||
aliyun_captcha_access_key_secret: "ak-secret-value",
|
||||
aliyun_captcha_region: "cn",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("关闭人机验证总开关会同时关闭所有服务商", async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...baseSettingsResponse,
|
||||
tencent_captcha_enabled: true,
|
||||
tencent_captcha_app_id: "123456789",
|
||||
tencent_captcha_app_secret_key_configured: true,
|
||||
tencent_captcha_cloud_secret_id_configured: true,
|
||||
tencent_captcha_cloud_secret_key_configured: true,
|
||||
});
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
await openSecurityTab(wrapper);
|
||||
|
||||
const masterToggle = wrapper.get('[data-testid="captcha-enabled-toggle"]');
|
||||
expect((masterToggle.element as HTMLInputElement).checked).toBe(true);
|
||||
// 加载后选中项跟随已启用的服务商
|
||||
expect(wrapper.text()).toContain("admin.settings.tencentCaptcha.appId");
|
||||
|
||||
await masterToggle.setValue(false);
|
||||
await wrapper.find("form").trigger("submit.prevent");
|
||||
await flushPromises();
|
||||
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
turnstile_enabled: false,
|
||||
tencent_captcha_enabled: false,
|
||||
aliyun_captcha_enabled: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("disables passkey sign-in when the RP configuration is unavailable", async () => {
|
||||
getSettings.mockResolvedValueOnce({
|
||||
...baseSettingsResponse,
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Turnstile Widget for Resend -->
|
||||
<div v-if="tencentCaptchaEnabled || (turnstileEnabled && showResendTurnstile)">
|
||||
<div v-if="actionCaptchaEnabled || (turnstileEnabled && showResendTurnstile)">
|
||||
<TurnstileWidget
|
||||
ref="turnstileRef"
|
||||
:site-key="turnstileSiteKey"
|
||||
@@ -75,6 +75,10 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
:aliyun-region="aliyunCaptchaRegion"
|
||||
@verify="onTurnstileVerify"
|
||||
@expire="onTurnstileExpire"
|
||||
@error="onTurnstileError"
|
||||
@@ -89,6 +93,10 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
:aliyun-region="aliyunCaptchaRegion"
|
||||
@verify="onCreateAccountTurnstileVerify"
|
||||
@expire="onCreateAccountTurnstileExpire"
|
||||
@error="onCreateAccountTurnstileError"
|
||||
@@ -254,6 +262,10 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
const aliyunCaptchaRegion = ref<string>('cn')
|
||||
const siteName = ref<string>('Sub2API')
|
||||
const registrationEmailSuffixWhitelist = ref<string[]>([])
|
||||
|
||||
@@ -265,10 +277,21 @@ const resendTencentCaptchaRandstr = ref<string>('')
|
||||
const createAccountTurnstileToken = ref<string>('')
|
||||
const createAccountTencentCaptchaRandstr = ref<string>('')
|
||||
const showResendTurnstile = ref<boolean>(false)
|
||||
const aliyunCaptchaReady = computed(
|
||||
() =>
|
||||
aliyunCaptchaEnabled.value &&
|
||||
Boolean(aliyunCaptchaSceneId.value) &&
|
||||
Boolean(aliyunCaptchaPrefix.value)
|
||||
)
|
||||
// 动作触发式验证码(腾讯/阿里云):重发验证码、创建账号时弹窗验证
|
||||
const actionCaptchaEnabled = computed(
|
||||
() =>
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value)) ||
|
||||
aliyunCaptchaReady.value
|
||||
)
|
||||
const captchaEnabled = computed(
|
||||
() =>
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) ||
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value))
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) || actionCaptchaEnabled.value
|
||||
)
|
||||
|
||||
const errors = ref({
|
||||
@@ -338,6 +361,10 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
aliyunCaptchaRegion.value = settings.aliyun_captcha_region || 'cn'
|
||||
siteName.value = settings.site_name || 'Sub2API'
|
||||
registrationEmailSuffixWhitelist.value = normalizeRegistrationEmailSuffixWhitelist(
|
||||
settings.registration_email_suffix_whitelist || []
|
||||
@@ -424,24 +451,24 @@ function resetCreateAccountTurnstile(): void {
|
||||
createAccountTurnstileRef.value?.reset()
|
||||
}
|
||||
|
||||
async function acquireResendTencentProof(): Promise<boolean> {
|
||||
if (!tencentCaptchaEnabled.value) return true
|
||||
async function acquireResendActionProof(): Promise<boolean> {
|
||||
if (!actionCaptchaEnabled.value) return true
|
||||
|
||||
const proof = await turnstileRef.value?.verifyTencent()
|
||||
const proof = await turnstileRef.value?.verifyAction()
|
||||
if (!proof) return false
|
||||
|
||||
resendTurnstileToken.value = proof.ticket
|
||||
resendTurnstileToken.value = proof.token
|
||||
resendTencentCaptchaRandstr.value = proof.randstr
|
||||
return true
|
||||
}
|
||||
|
||||
async function acquireCreateAccountTencentProof(): Promise<boolean> {
|
||||
if (!isPendingOAuthFlow() || !tencentCaptchaEnabled.value) return true
|
||||
async function acquireCreateAccountActionProof(): Promise<boolean> {
|
||||
if (!isPendingOAuthFlow() || !actionCaptchaEnabled.value) return true
|
||||
|
||||
const proof = await createAccountTurnstileRef.value?.verifyTencent()
|
||||
const proof = await createAccountTurnstileRef.value?.verifyAction()
|
||||
if (!proof) return false
|
||||
|
||||
createAccountTurnstileToken.value = proof.ticket
|
||||
createAccountTurnstileToken.value = proof.token
|
||||
createAccountTencentCaptchaRandstr.value = proof.randstr
|
||||
return true
|
||||
}
|
||||
@@ -505,9 +532,10 @@ async function sendCode(): Promise<void> {
|
||||
email: email.value,
|
||||
[pendingAuthTokenField.value]: pendingAuthToken.value || undefined,
|
||||
// 优先使用重发时新获取的 token(因为初始 token 可能已被使用)
|
||||
turnstile_token: turnstileEnabled.value
|
||||
? resendTurnstileToken.value || initialTurnstileToken.value || undefined
|
||||
: undefined,
|
||||
turnstile_token:
|
||||
turnstileEnabled.value || aliyunCaptchaEnabled.value
|
||||
? resendTurnstileToken.value || initialTurnstileToken.value || undefined
|
||||
: undefined,
|
||||
tencent_captcha_ticket: tencentCaptchaEnabled.value
|
||||
? resendTurnstileToken.value || initialTurnstileToken.value || undefined
|
||||
: undefined,
|
||||
@@ -593,7 +621,7 @@ async function handleResendCode(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireResendTencentProof())) {
|
||||
if (!(await acquireResendActionProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -629,7 +657,7 @@ async function handleVerify(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireCreateAccountTencentProof())) {
|
||||
if (!(await acquireCreateAccountActionProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -641,7 +669,8 @@ async function handleVerify(): Promise<void> {
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
verify_code: verifyCode.value.trim(),
|
||||
...(turnstileEnabled.value && createAccountTurnstileToken.value
|
||||
...((turnstileEnabled.value || aliyunCaptchaEnabled.value) &&
|
||||
createAccountTurnstileToken.value
|
||||
? { turnstile_token: createAccountTurnstileToken.value }
|
||||
: {}),
|
||||
...(tencentCaptchaEnabled.value && createAccountTurnstileToken.value
|
||||
@@ -685,7 +714,10 @@ async function handleVerify(): Promise<void> {
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
verify_code: verifyCode.value.trim(),
|
||||
turnstile_token: turnstileEnabled.value ? initialTurnstileToken.value || undefined : undefined,
|
||||
turnstile_token:
|
||||
turnstileEnabled.value || aliyunCaptchaEnabled.value
|
||||
? initialTurnstileToken.value || undefined
|
||||
: undefined,
|
||||
tencent_captcha_ticket: tencentCaptchaEnabled.value ? initialTurnstileToken.value || undefined : undefined,
|
||||
tencent_captcha_randstr: tencentCaptchaEnabled.value ? initialTencentCaptchaRandstr.value || undefined : undefined,
|
||||
promo_code: promoCode.value || undefined,
|
||||
|
||||
@@ -74,6 +74,10 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
:aliyun-region="aliyunCaptchaRegion"
|
||||
@verify="onTurnstileVerify"
|
||||
@expire="onTurnstileExpire"
|
||||
@error="onTurnstileError"
|
||||
@@ -153,15 +157,30 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
const aliyunCaptchaRegion = ref<string>('cn')
|
||||
|
||||
// Turnstile
|
||||
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
|
||||
const turnstileToken = ref<string>('')
|
||||
const tencentCaptchaRandstr = ref<string>('')
|
||||
const aliyunCaptchaReady = computed(
|
||||
() =>
|
||||
aliyunCaptchaEnabled.value &&
|
||||
Boolean(aliyunCaptchaSceneId.value) &&
|
||||
Boolean(aliyunCaptchaPrefix.value)
|
||||
)
|
||||
// 动作触发式验证码(腾讯/阿里云):提交时弹窗验证
|
||||
const actionCaptchaEnabled = computed(
|
||||
() =>
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value)) ||
|
||||
aliyunCaptchaReady.value
|
||||
)
|
||||
const captchaEnabled = computed(
|
||||
() =>
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) ||
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value))
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) || actionCaptchaEnabled.value
|
||||
)
|
||||
|
||||
const formData = reactive({
|
||||
@@ -190,6 +209,10 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
aliyunCaptchaRegion.value = settings.aliyun_captcha_region || 'cn'
|
||||
} catch (error) {
|
||||
console.error('Failed to load public settings:', error)
|
||||
}
|
||||
@@ -222,13 +245,13 @@ function resetCaptchaProof(): void {
|
||||
errors.turnstile = ''
|
||||
}
|
||||
|
||||
async function acquireTencentProof(): Promise<boolean> {
|
||||
if (!tencentCaptchaEnabled.value) return true
|
||||
async function acquireActionProof(): Promise<boolean> {
|
||||
if (!actionCaptchaEnabled.value) return true
|
||||
|
||||
const proof = await turnstileRef.value?.verifyTencent()
|
||||
const proof = await turnstileRef.value?.verifyAction()
|
||||
if (!proof) return false
|
||||
|
||||
turnstileToken.value = proof.ticket
|
||||
turnstileToken.value = proof.token
|
||||
tencentCaptchaRandstr.value = proof.randstr
|
||||
return true
|
||||
}
|
||||
@@ -268,7 +291,7 @@ async function handleSubmit(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireTencentProof())) {
|
||||
if (!(await acquireActionProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -277,7 +300,8 @@ async function handleSubmit(): Promise<void> {
|
||||
try {
|
||||
await forgotPassword({
|
||||
email: formData.email,
|
||||
turnstile_token: turnstileEnabled.value ? turnstileToken.value : undefined,
|
||||
turnstile_token:
|
||||
turnstileEnabled.value || aliyunCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_ticket: tencentCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_randstr: tencentCaptchaEnabled.value ? tencentCaptchaRandstr.value : undefined
|
||||
})
|
||||
|
||||
@@ -86,6 +86,10 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
:aliyun-region="aliyunCaptchaRegion"
|
||||
@verify="onTurnstileVerify"
|
||||
@expire="onTurnstileExpire"
|
||||
@error="onTurnstileError"
|
||||
@@ -240,8 +244,8 @@ import {
|
||||
type OAuthLoginStart
|
||||
} from '@/api/auth'
|
||||
import type {
|
||||
ActionCaptchaRequestProof,
|
||||
LoginAgreementDocument,
|
||||
TencentCaptchaRequestProof,
|
||||
TotpLoginResponse
|
||||
} from '@/types'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
@@ -269,6 +273,10 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
const aliyunCaptchaRegion = ref<string>('cn')
|
||||
const linuxdoOAuthEnabled = ref<boolean>(false)
|
||||
const dingtalkOAuthEnabled = ref<boolean>(false)
|
||||
const wechatOAuthEnabled = ref<boolean>(false)
|
||||
@@ -291,10 +299,21 @@ const showAgreementModal = ref<boolean>(false)
|
||||
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
|
||||
const turnstileToken = ref<string>('')
|
||||
const tencentCaptchaRandstr = ref<string>('')
|
||||
const aliyunCaptchaReady = computed(
|
||||
() =>
|
||||
aliyunCaptchaEnabled.value &&
|
||||
Boolean(aliyunCaptchaSceneId.value) &&
|
||||
Boolean(aliyunCaptchaPrefix.value)
|
||||
)
|
||||
// 动作触发式验证码(腾讯/阿里云):提交、OAuth 启动、passkey 时弹窗验证
|
||||
const actionCaptchaEnabled = computed(
|
||||
() =>
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value)) ||
|
||||
aliyunCaptchaReady.value
|
||||
)
|
||||
const captchaEnabled = computed(
|
||||
() =>
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) ||
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value))
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) || actionCaptchaEnabled.value
|
||||
)
|
||||
|
||||
// 2FA state
|
||||
@@ -364,6 +383,10 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
aliyunCaptchaRegion.value = settings.aliyun_captcha_region || 'cn'
|
||||
linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled
|
||||
dingtalkOAuthEnabled.value = settings.dingtalk_oauth_enabled ?? false
|
||||
wechatOAuthEnabled.value = isWeChatWebOAuthEnabled(settings)
|
||||
@@ -474,13 +497,13 @@ function resetCaptchaProof(): void {
|
||||
errors.turnstile = ''
|
||||
}
|
||||
|
||||
async function acquireTencentProof(): Promise<boolean> {
|
||||
if (!tencentCaptchaEnabled.value) return true
|
||||
async function acquireActionProof(): Promise<boolean> {
|
||||
if (!actionCaptchaEnabled.value) return true
|
||||
|
||||
const proof = await turnstileRef.value?.verifyTencent()
|
||||
const proof = await turnstileRef.value?.verifyAction()
|
||||
if (!proof) return false
|
||||
|
||||
turnstileToken.value = proof.ticket
|
||||
turnstileToken.value = proof.token
|
||||
tencentCaptchaRandstr.value = proof.randstr
|
||||
return true
|
||||
}
|
||||
@@ -541,18 +564,19 @@ async function handleLogin(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await acquireTencentProof())) {
|
||||
if (!(await acquireActionProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// Call auth store login
|
||||
// Call auth store login(阿里云 captchaVerifyParam 复用 turnstile_token 字段)
|
||||
const response = await authStore.login({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
turnstile_token: turnstileEnabled.value ? turnstileToken.value : undefined,
|
||||
turnstile_token:
|
||||
turnstileEnabled.value || aliyunCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_ticket: tencentCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_randstr: tencentCaptchaEnabled.value
|
||||
? tencentCaptchaRandstr.value
|
||||
@@ -600,14 +624,16 @@ async function handlePasskeyLogin(): Promise<void> {
|
||||
|
||||
passkeyLoading.value = true
|
||||
try {
|
||||
let proof: TencentCaptchaRequestProof | undefined
|
||||
if (tencentCaptchaEnabled.value) {
|
||||
const result = await turnstileRef.value?.verifyTencent()
|
||||
let proof: ActionCaptchaRequestProof | undefined
|
||||
if (actionCaptchaEnabled.value) {
|
||||
const result = await turnstileRef.value?.verifyAction()
|
||||
if (!result) return
|
||||
proof = {
|
||||
tencent_captcha_ticket: result.ticket,
|
||||
tencent_captcha_randstr: result.randstr
|
||||
}
|
||||
proof = tencentCaptchaEnabled.value
|
||||
? {
|
||||
tencent_captcha_ticket: result.token,
|
||||
tencent_captcha_randstr: result.randstr
|
||||
}
|
||||
: { turnstile_token: result.token }
|
||||
}
|
||||
|
||||
await authStore.loginWithPasskey(proof)
|
||||
@@ -622,7 +648,7 @@ async function handlePasskeyLogin(): Promise<void> {
|
||||
errorMessage.value = extractI18nErrorMessage(error, t, 'auth.errors', fallback)
|
||||
appStore.showError(errorMessage.value)
|
||||
} finally {
|
||||
if (tencentCaptchaEnabled.value) {
|
||||
if (actionCaptchaEnabled.value) {
|
||||
resetCaptchaProof()
|
||||
}
|
||||
passkeyLoading.value = false
|
||||
@@ -632,20 +658,25 @@ async function handlePasskeyLogin(): Promise<void> {
|
||||
async function handleOAuthStart(request: OAuthLoginStart): Promise<void> {
|
||||
if (authActionDisabled.value) return
|
||||
|
||||
if (!tencentCaptchaEnabled.value) {
|
||||
if (!actionCaptchaEnabled.value) {
|
||||
window.location.href = buildOAuthLoginStartURL(request)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const proof = await turnstileRef.value?.verifyTencent()
|
||||
const proof = await turnstileRef.value?.verifyAction()
|
||||
if (!proof) return
|
||||
|
||||
const result = await startOAuthLogin(request, {
|
||||
tencent_captcha_ticket: proof.ticket,
|
||||
tencent_captcha_randstr: proof.randstr
|
||||
})
|
||||
const result = await startOAuthLogin(
|
||||
request,
|
||||
tencentCaptchaEnabled.value
|
||||
? {
|
||||
tencent_captcha_ticket: proof.token,
|
||||
tencent_captcha_randstr: proof.randstr
|
||||
}
|
||||
: { turnstile_token: proof.token }
|
||||
)
|
||||
window.location.href = result.authorize_url
|
||||
} catch (error: unknown) {
|
||||
errorMessage.value = extractI18nErrorMessage(
|
||||
|
||||
@@ -211,6 +211,10 @@
|
||||
:turnstile-site-key="turnstileSiteKey"
|
||||
:tencent-enabled="tencentCaptchaEnabled"
|
||||
:tencent-app-id="tencentCaptchaAppId"
|
||||
:aliyun-enabled="aliyunCaptchaEnabled"
|
||||
:aliyun-scene-id="aliyunCaptchaSceneId"
|
||||
:aliyun-prefix="aliyunCaptchaPrefix"
|
||||
:aliyun-region="aliyunCaptchaRegion"
|
||||
@verify="onTurnstileVerify"
|
||||
@expire="onTurnstileExpire"
|
||||
@error="onTurnstileError"
|
||||
@@ -388,6 +392,10 @@ const turnstileEnabled = ref<boolean>(false)
|
||||
const turnstileSiteKey = ref<string>('')
|
||||
const tencentCaptchaEnabled = ref<boolean>(false)
|
||||
const tencentCaptchaAppId = ref<string>('')
|
||||
const aliyunCaptchaEnabled = ref<boolean>(false)
|
||||
const aliyunCaptchaSceneId = ref<string>('')
|
||||
const aliyunCaptchaPrefix = ref<string>('')
|
||||
const aliyunCaptchaRegion = ref<string>('cn')
|
||||
const siteName = ref<string>('Sub2API')
|
||||
const linuxdoOAuthEnabled = ref<boolean>(false)
|
||||
const wechatOAuthEnabled = ref<boolean>(false)
|
||||
@@ -408,10 +416,21 @@ const showAgreementModal = ref<boolean>(false)
|
||||
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
|
||||
const turnstileToken = ref<string>('')
|
||||
const tencentCaptchaRandstr = ref<string>('')
|
||||
const aliyunCaptchaReady = computed(
|
||||
() =>
|
||||
aliyunCaptchaEnabled.value &&
|
||||
Boolean(aliyunCaptchaSceneId.value) &&
|
||||
Boolean(aliyunCaptchaPrefix.value)
|
||||
)
|
||||
// 动作触发式验证码(腾讯/阿里云):提交、OAuth 启动时弹窗验证
|
||||
const actionCaptchaEnabled = computed(
|
||||
() =>
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value)) ||
|
||||
aliyunCaptchaReady.value
|
||||
)
|
||||
const captchaEnabled = computed(
|
||||
() =>
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) ||
|
||||
(tencentCaptchaEnabled.value && Boolean(tencentCaptchaAppId.value))
|
||||
(turnstileEnabled.value && Boolean(turnstileSiteKey.value)) || actionCaptchaEnabled.value
|
||||
)
|
||||
|
||||
// Promo code validation
|
||||
@@ -505,6 +524,10 @@ onMounted(async () => {
|
||||
turnstileSiteKey.value = settings.turnstile_site_key || ''
|
||||
tencentCaptchaEnabled.value = settings.tencent_captcha_enabled === true
|
||||
tencentCaptchaAppId.value = settings.tencent_captcha_app_id || ''
|
||||
aliyunCaptchaEnabled.value = settings.aliyun_captcha_enabled === true
|
||||
aliyunCaptchaSceneId.value = settings.aliyun_captcha_scene_id || ''
|
||||
aliyunCaptchaPrefix.value = settings.aliyun_captcha_prefix || ''
|
||||
aliyunCaptchaRegion.value = settings.aliyun_captcha_region || 'cn'
|
||||
siteName.value = settings.site_name || 'Sub2API'
|
||||
linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled
|
||||
wechatOAuthEnabled.value = isWeChatWebOAuthEnabled(settings)
|
||||
@@ -778,13 +801,13 @@ function resetCaptchaProof(): void {
|
||||
errors.turnstile = ''
|
||||
}
|
||||
|
||||
async function acquireTencentProof(): Promise<boolean> {
|
||||
if (!tencentCaptchaEnabled.value) return true
|
||||
async function acquireActionProof(): Promise<boolean> {
|
||||
if (!actionCaptchaEnabled.value) return true
|
||||
|
||||
const proof = await turnstileRef.value?.verifyTencent()
|
||||
const proof = await turnstileRef.value?.verifyAction()
|
||||
if (!proof) return false
|
||||
|
||||
turnstileToken.value = proof.ticket
|
||||
turnstileToken.value = proof.token
|
||||
tencentCaptchaRandstr.value = proof.randstr
|
||||
return true
|
||||
}
|
||||
@@ -792,20 +815,25 @@ async function acquireTencentProof(): Promise<boolean> {
|
||||
async function handleOAuthStart(request: OAuthLoginStart): Promise<void> {
|
||||
if (registrationActionDisabled.value) return
|
||||
|
||||
if (!tencentCaptchaEnabled.value) {
|
||||
if (!actionCaptchaEnabled.value) {
|
||||
window.location.href = buildOAuthLoginStartURL(request)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const proof = await turnstileRef.value?.verifyTencent()
|
||||
const proof = await turnstileRef.value?.verifyAction()
|
||||
if (!proof) return
|
||||
|
||||
const result = await startOAuthLogin(request, {
|
||||
tencent_captcha_ticket: proof.ticket,
|
||||
tencent_captcha_randstr: proof.randstr
|
||||
})
|
||||
const result = await startOAuthLogin(
|
||||
request,
|
||||
tencentCaptchaEnabled.value
|
||||
? {
|
||||
tencent_captcha_ticket: proof.token,
|
||||
tencent_captcha_randstr: proof.randstr
|
||||
}
|
||||
: { turnstile_token: proof.token }
|
||||
)
|
||||
window.location.href = result.authorize_url
|
||||
} catch (error: unknown) {
|
||||
errorMessage.value = extractI18nErrorMessage(
|
||||
@@ -950,7 +978,7 @@ async function handleRegister(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await acquireTencentProof())) {
|
||||
if (!(await acquireActionProof())) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -970,7 +998,8 @@ async function handleRegister(): Promise<void> {
|
||||
JSON.stringify({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
turnstile_token: turnstileEnabled.value ? turnstileToken.value : undefined,
|
||||
turnstile_token:
|
||||
turnstileEnabled.value || aliyunCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_ticket: tencentCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_randstr: tencentCaptchaEnabled.value ? tencentCaptchaRandstr.value : undefined,
|
||||
promo_code: formData.promo_code || undefined,
|
||||
@@ -988,7 +1017,8 @@ async function handleRegister(): Promise<void> {
|
||||
await authStore.register({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
turnstile_token: turnstileEnabled.value ? turnstileToken.value : undefined,
|
||||
turnstile_token:
|
||||
turnstileEnabled.value || aliyunCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_ticket: tencentCaptchaEnabled.value ? turnstileToken.value : undefined,
|
||||
tencent_captcha_randstr: tencentCaptchaEnabled.value ? tencentCaptchaRandstr.value : undefined,
|
||||
promo_code: formData.promo_code || undefined,
|
||||
|
||||
@@ -18,7 +18,7 @@ const {
|
||||
apiClientPostMock,
|
||||
authStoreState,
|
||||
createTurnstileResetMock,
|
||||
verifyTencentMock,
|
||||
verifyActionMock,
|
||||
} = vi.hoisted(() => ({
|
||||
pushMock: vi.fn(),
|
||||
showSuccessMock: vi.fn(),
|
||||
@@ -33,7 +33,7 @@ const {
|
||||
persistOAuthTokenContextMock: vi.fn(),
|
||||
apiClientPostMock: vi.fn(),
|
||||
createTurnstileResetMock: vi.fn(),
|
||||
verifyTencentMock: vi.fn(),
|
||||
verifyActionMock: vi.fn(),
|
||||
authStoreState: {
|
||||
pendingAuthSession: null as null | {
|
||||
token: string
|
||||
@@ -116,7 +116,7 @@ describe('EmailVerifyView', () => {
|
||||
persistOAuthTokenContextMock.mockReset()
|
||||
apiClientPostMock.mockReset()
|
||||
createTurnstileResetMock.mockReset()
|
||||
verifyTencentMock.mockReset()
|
||||
verifyActionMock.mockReset()
|
||||
authStoreState.pendingAuthSession = null
|
||||
sessionStorage.clear()
|
||||
localStorage.clear()
|
||||
@@ -142,9 +142,9 @@ describe('EmailVerifyView', () => {
|
||||
registration_email_suffix_whitelist: [],
|
||||
})
|
||||
sendVerifyCodeMock.mockResolvedValue({ countdown: 0 })
|
||||
verifyTencentMock
|
||||
.mockResolvedValueOnce({ ticket: 'ticket-1', randstr: '@rand-1' })
|
||||
.mockResolvedValueOnce({ ticket: 'ticket-2', randstr: '@rand-2' })
|
||||
verifyActionMock
|
||||
.mockResolvedValueOnce({ token: 'ticket-1', randstr: '@rand-1' })
|
||||
.mockResolvedValueOnce({ token: 'ticket-2', randstr: '@rand-2' })
|
||||
sessionStorage.setItem(
|
||||
'register_data',
|
||||
JSON.stringify({
|
||||
@@ -157,7 +157,7 @@ describe('EmailVerifyView', () => {
|
||||
|
||||
const CaptchaChallengeStub = defineComponent({
|
||||
setup(_, { expose }) {
|
||||
expose({ verifyTencent: verifyTencentMock, reset: createTurnstileResetMock })
|
||||
expose({ verifyAction: verifyActionMock, reset: createTurnstileResetMock })
|
||||
return () => h('div')
|
||||
},
|
||||
})
|
||||
@@ -182,7 +182,7 @@ describe('EmailVerifyView', () => {
|
||||
await resendButton().trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyTencentMock).toHaveBeenCalledTimes(2)
|
||||
expect(verifyActionMock).toHaveBeenCalledTimes(2)
|
||||
expect(sendVerifyCodeMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
tencent_captcha_ticket: 'ticket-1',
|
||||
tencent_captcha_randstr: '@rand-1',
|
||||
|
||||
@@ -7,7 +7,7 @@ const loginMock = vi.fn()
|
||||
const loginWithPasskeyMock = vi.fn()
|
||||
const getPublicSettingsMock = vi.fn()
|
||||
const startOAuthLoginMock = vi.fn()
|
||||
const verifyTencentMock = vi.fn()
|
||||
const verifyActionMock = vi.fn()
|
||||
const captchaResetMock = vi.fn()
|
||||
const locationState = { href: 'http://localhost/login' }
|
||||
|
||||
@@ -54,7 +54,7 @@ vi.mock('@/api/auth', async () => {
|
||||
const CaptchaChallengeStub = defineComponent({
|
||||
setup(_, { expose }) {
|
||||
expose({
|
||||
verifyTencent: verifyTencentMock,
|
||||
verifyAction: verifyActionMock,
|
||||
reset: captchaResetMock
|
||||
})
|
||||
return () => h('div')
|
||||
@@ -101,7 +101,7 @@ describe('Tencent captcha action gate', () => {
|
||||
loginWithPasskeyMock.mockReset()
|
||||
getPublicSettingsMock.mockReset()
|
||||
startOAuthLoginMock.mockReset()
|
||||
verifyTencentMock.mockReset()
|
||||
verifyActionMock.mockReset()
|
||||
captchaResetMock.mockReset()
|
||||
getPublicSettingsMock.mockResolvedValue({
|
||||
turnstile_enabled: false,
|
||||
@@ -117,7 +117,7 @@ describe('Tencent captcha action gate', () => {
|
||||
loginMock.mockResolvedValue({})
|
||||
loginWithPasskeyMock.mockResolvedValue({})
|
||||
startOAuthLoginMock.mockResolvedValue({ authorize_url: 'https://github.example/authorize' })
|
||||
verifyTencentMock.mockResolvedValue({ ticket: 'ticket-1', randstr: '@rand-1' })
|
||||
verifyActionMock.mockResolvedValue({ token: 'ticket-1', randstr: '@rand-1' })
|
||||
Object.defineProperty(window, 'PublicKeyCredential', {
|
||||
configurable: true,
|
||||
value: class PublicKeyCredential {}
|
||||
@@ -138,7 +138,7 @@ describe('Tencent captcha action gate', () => {
|
||||
await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyTencentMock).toHaveBeenCalledOnce()
|
||||
expect(verifyActionMock).toHaveBeenCalledOnce()
|
||||
expect(loginMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
tencent_captcha_ticket: 'ticket-1',
|
||||
tencent_captcha_randstr: '@rand-1'
|
||||
@@ -146,7 +146,7 @@ describe('Tencent captcha action gate', () => {
|
||||
})
|
||||
|
||||
it('does not call login when Tencent captcha is closed', async () => {
|
||||
verifyTencentMock.mockResolvedValue(null)
|
||||
verifyActionMock.mockResolvedValue(null)
|
||||
const wrapper = mountLogin()
|
||||
await flushPromises()
|
||||
await wrapper.get('#email').setValue('user@example.com')
|
||||
@@ -155,7 +155,7 @@ describe('Tencent captcha action gate', () => {
|
||||
await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyTencentMock).toHaveBeenCalledOnce()
|
||||
expect(verifyActionMock).toHaveBeenCalledOnce()
|
||||
expect(loginMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('Tencent captcha action gate', () => {
|
||||
await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyTencentMock).not.toHaveBeenCalled()
|
||||
expect(verifyActionMock).not.toHaveBeenCalled()
|
||||
expect(loginMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('Tencent captcha action gate', () => {
|
||||
await wrapper.get('[data-testid="oauth-start"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyTencentMock).toHaveBeenCalledOnce()
|
||||
expect(verifyActionMock).toHaveBeenCalledOnce()
|
||||
expect(startOAuthLoginMock).toHaveBeenCalledWith(
|
||||
{ provider: 'github', params: { redirect: '/dashboard' } },
|
||||
{
|
||||
@@ -190,7 +190,7 @@ describe('Tencent captcha action gate', () => {
|
||||
})
|
||||
|
||||
it('does not start OAuth when Tencent captcha is closed', async () => {
|
||||
verifyTencentMock.mockResolvedValue(null)
|
||||
verifyActionMock.mockResolvedValue(null)
|
||||
const wrapper = mountLogin()
|
||||
await flushPromises()
|
||||
|
||||
@@ -208,7 +208,7 @@ describe('Tencent captcha action gate', () => {
|
||||
await wrapper.get('button.btn-secondary.w-full').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyTencentMock).toHaveBeenCalledOnce()
|
||||
expect(verifyActionMock).toHaveBeenCalledOnce()
|
||||
expect(loginWithPasskeyMock).toHaveBeenCalledWith({
|
||||
tencent_captcha_ticket: 'ticket-1',
|
||||
tencent_captcha_randstr: '@rand-1'
|
||||
@@ -217,7 +217,7 @@ describe('Tencent captcha action gate', () => {
|
||||
})
|
||||
|
||||
it('does not invoke Passkey when Tencent captcha is closed', async () => {
|
||||
verifyTencentMock.mockResolvedValue(null)
|
||||
verifyActionMock.mockResolvedValue(null)
|
||||
const wrapper = mountLogin()
|
||||
await flushPromises()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user