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 +} 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') - }} -
-
+ +
@@ -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') }} +

+
+