From 2c35f0276f4cb57151f36ade7e5e2cba186c2551 Mon Sep 17 00:00:00 2001
From: shaw
Date: Wed, 31 Dec 2025 20:46:54 +0800
Subject: [PATCH 1/2] =?UTF-8?q?fix(frontend):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E6=97=A0=E9=99=90=E5=88=B6=E8=AE=A2=E9=98=85=E7=9A=84=E6=98=BE?=
=?UTF-8?q?=E7=A4=BA=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../common/SubscriptionProgressMini.vue | 188 ++++++++++--------
frontend/src/i18n/locales/en.ts | 5 +-
frontend/src/i18n/locales/zh.ts | 5 +-
.../src/views/admin/SubscriptionsView.vue | 9 +-
frontend/src/views/user/SubscriptionsView.vue | 18 +-
5 files changed, 134 insertions(+), 91 deletions(-)
diff --git a/frontend/src/components/common/SubscriptionProgressMini.vue b/frontend/src/components/common/SubscriptionProgressMini.vue
index b84175e9..92198c2c 100644
--- a/frontend/src/components/common/SubscriptionProgressMini.vue
+++ b/frontend/src/components/common/SubscriptionProgressMini.vue
@@ -69,94 +69,108 @@
-
+
-
-
{{
- t('subscriptionProgress.daily')
- }}
-
-
- {{
- formatUsage(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
- }}
+
+
+ ∞
+
+ {{ t('subscriptionProgress.unlimited') }}
-
-
{{
- t('subscriptionProgress.weekly')
- }}
-
-
+
+
+
+
{{
+ t('subscriptionProgress.daily')
+ }}
+
+
+ {{
+ formatUsage(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
+ }}
+
-
- {{
- formatUsage(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
- }}
-
-
-
-
{{
- t('subscriptionProgress.monthly')
- }}
-
-
+
+
{{
+ t('subscriptionProgress.weekly')
+ }}
+
+
+ {{
+ formatUsage(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
+ }}
+
-
- {{
- formatUsage(
- subscription.monthly_usage_usd,
- subscription.group?.monthly_limit_usd
- )
- }}
-
-
+
+
+
{{
+ t('subscriptionProgress.monthly')
+ }}
+
+
+ {{
+ formatUsage(
+ subscription.monthly_usage_usd,
+ subscription.group?.monthly_limit_usd
+ )
+ }}
+
+
+
@@ -215,7 +229,19 @@ function getMaxUsagePercentage(sub: UserSubscription): number {
return percentages.length > 0 ? Math.max(...percentages) : 0
}
+function isUnlimited(sub: UserSubscription): boolean {
+ return (
+ !sub.group?.daily_limit_usd &&
+ !sub.group?.weekly_limit_usd &&
+ !sub.group?.monthly_limit_usd
+ )
+}
+
function getProgressDotClass(sub: UserSubscription): string {
+ // Unlimited subscriptions get a special color
+ if (isUnlimited(sub)) {
+ return 'bg-emerald-500'
+ }
const maxPercentage = getMaxUsagePercentage(sub)
if (maxPercentage >= 90) return 'bg-red-500'
if (maxPercentage >= 70) return 'bg-orange-500'
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index d153b553..6d1095cf 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -749,6 +749,7 @@ export default {
weekly: 'Weekly',
monthly: 'Monthly',
noLimits: 'No limits configured',
+ unlimited: 'Unlimited',
resetNow: 'Resetting soon',
windowNotActive: 'Window not active',
resetInMinutes: 'Resets in {minutes}m',
@@ -1492,7 +1493,8 @@ export default {
expiresToday: 'Expires today',
expiresTomorrow: 'Expires tomorrow',
viewAll: 'View all subscriptions',
- noSubscriptions: 'No active subscriptions'
+ noSubscriptions: 'No active subscriptions',
+ unlimited: 'Unlimited'
},
// Version Badge
@@ -1535,6 +1537,7 @@ export default {
expires: 'Expires',
noExpiration: 'No expiration',
unlimited: 'Unlimited',
+ unlimitedDesc: 'No usage limits on this subscription',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index c6105683..97d57051 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -840,6 +840,7 @@ export default {
weekly: '每周',
monthly: '每月',
noLimits: '未配置限额',
+ unlimited: '无限制',
resetNow: '即将重置',
windowNotActive: '窗口未激活',
resetInMinutes: '{minutes} 分钟后重置',
@@ -1689,7 +1690,8 @@ export default {
expiresToday: '今天到期',
expiresTomorrow: '明天到期',
viewAll: '查看全部订阅',
- noSubscriptions: '暂无有效订阅'
+ noSubscriptions: '暂无有效订阅',
+ unlimited: '无限制'
},
// Version Badge
@@ -1731,6 +1733,7 @@ export default {
expires: '到期时间',
noExpiration: '无到期时间',
unlimited: '无限制',
+ unlimitedDesc: '该订阅无用量限制',
daily: '每日',
weekly: '每周',
monthly: '每月',
diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue
index bd6a17eb..679c3275 100644
--- a/frontend/src/views/admin/SubscriptionsView.vue
+++ b/frontend/src/views/admin/SubscriptionsView.vue
@@ -202,16 +202,19 @@
-
+
- {{ t('admin.subscriptions.noLimits') }}
+ ∞
+
+ {{ t('admin.subscriptions.unlimited') }}
+
diff --git a/frontend/src/views/user/SubscriptionsView.vue b/frontend/src/views/user/SubscriptionsView.vue
index dc93a9c1..b03b665a 100644
--- a/frontend/src/views/user/SubscriptionsView.vue
+++ b/frontend/src/views/user/SubscriptionsView.vue
@@ -230,18 +230,26 @@
-
+
-
{{
- t('userSubscriptions.unlimited')
- }}
+
+
∞
+
+
+ {{ t('userSubscriptions.unlimited') }}
+
+
+ {{ t('userSubscriptions.unlimitedDesc') }}
+
+
+
From 0b6371174e18ab03848b1358566a85694f46a2eb Mon Sep 17 00:00:00 2001
From: shaw
Date: Wed, 31 Dec 2025 21:05:33 +0800
Subject: [PATCH 2/2] =?UTF-8?q?fix(settings):=20=E4=BF=9D=E5=AD=98=20Turns?=
=?UTF-8?q?tile=20=E8=AE=BE=E7=BD=AE=E6=97=B6=E9=AA=8C=E8=AF=81=E5=8F=82?=
=?UTF-8?q?=E6=95=B0=E6=9C=89=E6=95=88=E6=80=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/cmd/server/wire_gen.go | 2 +-
.../internal/handler/admin/setting_handler.go | 42 ++++++++++++++++---
backend/internal/server/api_contract_test.go | 2 +-
backend/internal/service/turnstile_service.go | 20 +++++++++
4 files changed, 59 insertions(+), 7 deletions(-)
diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go
index d469dcbb..c4859383 100644
--- a/backend/cmd/server/wire_gen.go
+++ b/backend/cmd/server/wire_gen.go
@@ -109,7 +109,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService)
proxyHandler := admin.NewProxyHandler(adminService)
adminRedeemHandler := admin.NewRedeemHandler(adminService)
- settingHandler := admin.NewSettingHandler(settingService, emailService)
+ settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService)
updateCache := repository.NewUpdateCache(redisClient)
gitHubReleaseClient := repository.NewGitHubReleaseClient()
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go
index 14b569de..e533aef1 100644
--- a/backend/internal/handler/admin/setting_handler.go
+++ b/backend/internal/handler/admin/setting_handler.go
@@ -10,15 +10,17 @@ import (
// SettingHandler 系统设置处理器
type SettingHandler struct {
- settingService *service.SettingService
- emailService *service.EmailService
+ settingService *service.SettingService
+ emailService *service.EmailService
+ turnstileService *service.TurnstileService
}
// NewSettingHandler 创建系统设置处理器
-func NewSettingHandler(settingService *service.SettingService, emailService *service.EmailService) *SettingHandler {
+func NewSettingHandler(settingService *service.SettingService, emailService *service.EmailService, turnstileService *service.TurnstileService) *SettingHandler {
return &SettingHandler{
- settingService: settingService,
- emailService: emailService,
+ settingService: settingService,
+ emailService: emailService,
+ turnstileService: turnstileService,
}
}
@@ -108,6 +110,36 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
req.SmtpPort = 587
}
+ // Turnstile 参数验证
+ if req.TurnstileEnabled {
+ // 检查必填字段
+ if req.TurnstileSiteKey == "" {
+ response.BadRequest(c, "Turnstile Site Key is required when enabled")
+ return
+ }
+ if req.TurnstileSecretKey == "" {
+ response.BadRequest(c, "Turnstile Secret Key is required when enabled")
+ return
+ }
+
+ // 获取当前设置,检查参数是否有变化
+ currentSettings, err := h.settingService.GetAllSettings(c.Request.Context())
+ if err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+
+ // 当 site_key 或 secret_key 任一变化时验证(避免配置错误导致无法登录)
+ siteKeyChanged := currentSettings.TurnstileSiteKey != req.TurnstileSiteKey
+ secretKeyChanged := currentSettings.TurnstileSecretKey != req.TurnstileSecretKey
+ if siteKeyChanged || secretKeyChanged {
+ if err := h.turnstileService.ValidateSecretKey(c.Request.Context(), req.TurnstileSecretKey); err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+ }
+ }
+
settings := &service.SystemSettings{
RegistrationEnabled: req.RegistrationEnabled,
EmailVerifyEnabled: req.EmailVerifyEnabled,
diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go
index 5a243bfc..3912c8fb 100644
--- a/backend/internal/server/api_contract_test.go
+++ b/backend/internal/server/api_contract_test.go
@@ -385,7 +385,7 @@ func newContractDeps(t *testing.T) *contractDeps {
authHandler := handler.NewAuthHandler(cfg, nil, userService)
apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
usageHandler := handler.NewUsageHandler(usageService, apiKeyService)
- adminSettingHandler := adminhandler.NewSettingHandler(settingService, nil)
+ adminSettingHandler := adminhandler.NewSettingHandler(settingService, nil, nil)
jwtAuth := func(c *gin.Context) {
c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
diff --git a/backend/internal/service/turnstile_service.go b/backend/internal/service/turnstile_service.go
index 2a68c11b..cfb87c57 100644
--- a/backend/internal/service/turnstile_service.go
+++ b/backend/internal/service/turnstile_service.go
@@ -11,6 +11,7 @@ import (
var (
ErrTurnstileVerificationFailed = infraerrors.BadRequest("TURNSTILE_VERIFICATION_FAILED", "turnstile verification failed")
ErrTurnstileNotConfigured = infraerrors.ServiceUnavailable("TURNSTILE_NOT_CONFIGURED", "turnstile not configured")
+ ErrTurnstileInvalidSecretKey = infraerrors.BadRequest("TURNSTILE_INVALID_SECRET_KEY", "invalid turnstile secret key")
)
// TurnstileVerifier 验证 Turnstile token 的接口
@@ -83,3 +84,22 @@ func (s *TurnstileService) VerifyToken(ctx context.Context, token string, remote
func (s *TurnstileService) IsEnabled(ctx context.Context) bool {
return s.settingService.IsTurnstileEnabled(ctx)
}
+
+// ValidateSecretKey 验证 Turnstile Secret Key 是否有效
+func (s *TurnstileService) ValidateSecretKey(ctx context.Context, secretKey string) error {
+ // 发送一个测试token的验证请求来检查secret_key是否有效
+ result, err := s.verifier.VerifyToken(ctx, secretKey, "test-validation", "")
+ if err != nil {
+ return fmt.Errorf("validate secret key: %w", err)
+ }
+
+ // 检查是否有 invalid-input-secret 错误
+ for _, code := range result.ErrorCodes {
+ if code == "invalid-input-secret" {
+ return ErrTurnstileInvalidSecretKey
+ }
+ }
+
+ // 其他错误(如 invalid-input-response)说明 secret key 是有效的
+ return nil
+}