Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
2ccdc2b8
Commit
2ccdc2b8
authored
Dec 31, 2025
by
IanShaw027
Browse files
Merge remote-tracking branch 'upstream/main'
parents
c1e25b7e
0b637117
Changes
9
Show whitespace changes
Inline
Side-by-side
backend/cmd/server/wire_gen.go
View file @
2ccdc2b8
...
@@ -109,7 +109,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -109,7 +109,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
antigravityOAuthHandler
:=
admin
.
NewAntigravityOAuthHandler
(
antigravityOAuthService
)
antigravityOAuthHandler
:=
admin
.
NewAntigravityOAuthHandler
(
antigravityOAuthService
)
proxyHandler
:=
admin
.
NewProxyHandler
(
adminService
)
proxyHandler
:=
admin
.
NewProxyHandler
(
adminService
)
adminRedeemHandler
:=
admin
.
NewRedeemHandler
(
adminService
)
adminRedeemHandler
:=
admin
.
NewRedeemHandler
(
adminService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
,
turnstileService
)
updateCache
:=
repository
.
NewUpdateCache
(
redisClient
)
updateCache
:=
repository
.
NewUpdateCache
(
redisClient
)
gitHubReleaseClient
:=
repository
.
NewGitHubReleaseClient
()
gitHubReleaseClient
:=
repository
.
NewGitHubReleaseClient
()
serviceBuildInfo
:=
provideServiceBuildInfo
(
buildInfo
)
serviceBuildInfo
:=
provideServiceBuildInfo
(
buildInfo
)
...
...
backend/internal/handler/admin/setting_handler.go
View file @
2ccdc2b8
...
@@ -12,13 +12,15 @@ import (
...
@@ -12,13 +12,15 @@ import (
type
SettingHandler
struct
{
type
SettingHandler
struct
{
settingService
*
service
.
SettingService
settingService
*
service
.
SettingService
emailService
*
service
.
EmailService
emailService
*
service
.
EmailService
turnstileService
*
service
.
TurnstileService
}
}
// NewSettingHandler 创建系统设置处理器
// NewSettingHandler 创建系统设置处理器
func
NewSettingHandler
(
settingService
*
service
.
SettingService
,
emailService
*
service
.
EmailService
)
*
SettingHandler
{
func
NewSettingHandler
(
settingService
*
service
.
SettingService
,
emailService
*
service
.
EmailService
,
turnstileService
*
service
.
TurnstileService
)
*
SettingHandler
{
return
&
SettingHandler
{
return
&
SettingHandler
{
settingService
:
settingService
,
settingService
:
settingService
,
emailService
:
emailService
,
emailService
:
emailService
,
turnstileService
:
turnstileService
,
}
}
}
}
...
@@ -108,6 +110,36 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -108,6 +110,36 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
req
.
SmtpPort
=
587
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
{
settings
:=
&
service
.
SystemSettings
{
RegistrationEnabled
:
req
.
RegistrationEnabled
,
RegistrationEnabled
:
req
.
RegistrationEnabled
,
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
EmailVerifyEnabled
:
req
.
EmailVerifyEnabled
,
...
...
backend/internal/server/api_contract_test.go
View file @
2ccdc2b8
...
@@ -385,7 +385,7 @@ func newContractDeps(t *testing.T) *contractDeps {
...
@@ -385,7 +385,7 @@ func newContractDeps(t *testing.T) *contractDeps {
authHandler
:=
handler
.
NewAuthHandler
(
cfg
,
nil
,
userService
)
authHandler
:=
handler
.
NewAuthHandler
(
cfg
,
nil
,
userService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
usageHandler
:=
handler
.
NewUsageHandler
(
usageService
,
apiKeyService
)
adminSettingHandler
:=
adminhandler
.
NewSettingHandler
(
settingService
,
nil
)
adminSettingHandler
:=
adminhandler
.
NewSettingHandler
(
settingService
,
nil
,
nil
)
jwtAuth
:=
func
(
c
*
gin
.
Context
)
{
jwtAuth
:=
func
(
c
*
gin
.
Context
)
{
c
.
Set
(
string
(
middleware
.
ContextKeyUser
),
middleware
.
AuthSubject
{
c
.
Set
(
string
(
middleware
.
ContextKeyUser
),
middleware
.
AuthSubject
{
...
...
backend/internal/service/turnstile_service.go
View file @
2ccdc2b8
...
@@ -11,6 +11,7 @@ import (
...
@@ -11,6 +11,7 @@ import (
var
(
var
(
ErrTurnstileVerificationFailed
=
infraerrors
.
BadRequest
(
"TURNSTILE_VERIFICATION_FAILED"
,
"turnstile verification failed"
)
ErrTurnstileVerificationFailed
=
infraerrors
.
BadRequest
(
"TURNSTILE_VERIFICATION_FAILED"
,
"turnstile verification failed"
)
ErrTurnstileNotConfigured
=
infraerrors
.
ServiceUnavailable
(
"TURNSTILE_NOT_CONFIGURED"
,
"turnstile not configured"
)
ErrTurnstileNotConfigured
=
infraerrors
.
ServiceUnavailable
(
"TURNSTILE_NOT_CONFIGURED"
,
"turnstile not configured"
)
ErrTurnstileInvalidSecretKey
=
infraerrors
.
BadRequest
(
"TURNSTILE_INVALID_SECRET_KEY"
,
"invalid turnstile secret key"
)
)
)
// TurnstileVerifier 验证 Turnstile token 的接口
// TurnstileVerifier 验证 Turnstile token 的接口
...
@@ -83,3 +84,22 @@ func (s *TurnstileService) VerifyToken(ctx context.Context, token string, remote
...
@@ -83,3 +84,22 @@ func (s *TurnstileService) VerifyToken(ctx context.Context, token string, remote
func
(
s
*
TurnstileService
)
IsEnabled
(
ctx
context
.
Context
)
bool
{
func
(
s
*
TurnstileService
)
IsEnabled
(
ctx
context
.
Context
)
bool
{
return
s
.
settingService
.
IsTurnstileEnabled
(
ctx
)
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
}
frontend/src/components/common/SubscriptionProgressMini.vue
View file @
2ccdc2b8
...
@@ -69,8 +69,21 @@
...
@@ -69,8 +69,21 @@
<
/span
>
<
/span
>
<
/div
>
<
/div
>
<!--
Progress
bars
-->
<!--
Progress
bars
or
Unlimited
badge
-->
<
div
class
=
"
space-y-1.5
"
>
<
div
class
=
"
space-y-1.5
"
>
<!--
Unlimited
subscription
badge
-->
<
div
v
-
if
=
"
isUnlimited(subscription)
"
class
=
"
flex items-center gap-2 rounded-lg bg-gradient-to-r from-emerald-50 to-teal-50 px-2.5 py-1.5 dark:from-emerald-900/20 dark:to-teal-900/20
"
>
<
span
class
=
"
text-lg text-emerald-600 dark:text-emerald-400
"
>
∞
<
/span
>
<
span
class
=
"
text-xs font-medium text-emerald-700 dark:text-emerald-300
"
>
{{
t
(
'
subscriptionProgress.unlimited
'
)
}}
<
/span
>
<
/div
>
<!--
Progress
bars
for
limited
subscriptions
-->
<
template
v
-
else
>
<
div
v
-
if
=
"
subscription.group?.daily_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
div
v
-
if
=
"
subscription.group?.daily_limit_usd
"
class
=
"
flex items-center gap-2
"
>
<
span
class
=
"
w-8 flex-shrink-0 text-[10px] text-gray-500
"
>
{{
<
span
class
=
"
w-8 flex-shrink-0 text-[10px] text-gray-500
"
>
{{
t
(
'
subscriptionProgress.daily
'
)
t
(
'
subscriptionProgress.daily
'
)
...
@@ -157,6 +170,7 @@
...
@@ -157,6 +170,7 @@
}}
}}
<
/span
>
<
/span
>
<
/div
>
<
/div
>
<
/template
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
@@ -215,7 +229,19 @@ function getMaxUsagePercentage(sub: UserSubscription): number {
...
@@ -215,7 +229,19 @@ function getMaxUsagePercentage(sub: UserSubscription): number {
return
percentages
.
length
>
0
?
Math
.
max
(...
percentages
)
:
0
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
{
function
getProgressDotClass
(
sub
:
UserSubscription
):
string
{
// Unlimited subscriptions get a special color
if
(
isUnlimited
(
sub
))
{
return
'
bg-emerald-500
'
}
const
maxPercentage
=
getMaxUsagePercentage
(
sub
)
const
maxPercentage
=
getMaxUsagePercentage
(
sub
)
if
(
maxPercentage
>=
90
)
return
'
bg-red-500
'
if
(
maxPercentage
>=
90
)
return
'
bg-red-500
'
if
(
maxPercentage
>=
70
)
return
'
bg-orange-500
'
if
(
maxPercentage
>=
70
)
return
'
bg-orange-500
'
...
...
frontend/src/i18n/locales/en.ts
View file @
2ccdc2b8
...
@@ -749,6 +749,7 @@ export default {
...
@@ -749,6 +749,7 @@ export default {
weekly
:
'
Weekly
'
,
weekly
:
'
Weekly
'
,
monthly
:
'
Monthly
'
,
monthly
:
'
Monthly
'
,
noLimits
:
'
No limits configured
'
,
noLimits
:
'
No limits configured
'
,
unlimited
:
'
Unlimited
'
,
resetNow
:
'
Resetting soon
'
,
resetNow
:
'
Resetting soon
'
,
windowNotActive
:
'
Window not active
'
,
windowNotActive
:
'
Window not active
'
,
resetInMinutes
:
'
Resets in {minutes}m
'
,
resetInMinutes
:
'
Resets in {minutes}m
'
,
...
@@ -1492,7 +1493,8 @@ export default {
...
@@ -1492,7 +1493,8 @@ export default {
expiresToday
:
'
Expires today
'
,
expiresToday
:
'
Expires today
'
,
expiresTomorrow
:
'
Expires tomorrow
'
,
expiresTomorrow
:
'
Expires tomorrow
'
,
viewAll
:
'
View all subscriptions
'
,
viewAll
:
'
View all subscriptions
'
,
noSubscriptions
:
'
No active subscriptions
'
noSubscriptions
:
'
No active subscriptions
'
,
unlimited
:
'
Unlimited
'
},
},
// Version Badge
// Version Badge
...
@@ -1535,6 +1537,7 @@ export default {
...
@@ -1535,6 +1537,7 @@ export default {
expires
:
'
Expires
'
,
expires
:
'
Expires
'
,
noExpiration
:
'
No expiration
'
,
noExpiration
:
'
No expiration
'
,
unlimited
:
'
Unlimited
'
,
unlimited
:
'
Unlimited
'
,
unlimitedDesc
:
'
No usage limits on this subscription
'
,
daily
:
'
Daily
'
,
daily
:
'
Daily
'
,
weekly
:
'
Weekly
'
,
weekly
:
'
Weekly
'
,
monthly
:
'
Monthly
'
,
monthly
:
'
Monthly
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
2ccdc2b8
...
@@ -840,6 +840,7 @@ export default {
...
@@ -840,6 +840,7 @@ export default {
weekly
:
'
每周
'
,
weekly
:
'
每周
'
,
monthly
:
'
每月
'
,
monthly
:
'
每月
'
,
noLimits
:
'
未配置限额
'
,
noLimits
:
'
未配置限额
'
,
unlimited
:
'
无限制
'
,
resetNow
:
'
即将重置
'
,
resetNow
:
'
即将重置
'
,
windowNotActive
:
'
窗口未激活
'
,
windowNotActive
:
'
窗口未激活
'
,
resetInMinutes
:
'
{minutes} 分钟后重置
'
,
resetInMinutes
:
'
{minutes} 分钟后重置
'
,
...
@@ -1689,7 +1690,8 @@ export default {
...
@@ -1689,7 +1690,8 @@ export default {
expiresToday
:
'
今天到期
'
,
expiresToday
:
'
今天到期
'
,
expiresTomorrow
:
'
明天到期
'
,
expiresTomorrow
:
'
明天到期
'
,
viewAll
:
'
查看全部订阅
'
,
viewAll
:
'
查看全部订阅
'
,
noSubscriptions
:
'
暂无有效订阅
'
noSubscriptions
:
'
暂无有效订阅
'
,
unlimited
:
'
无限制
'
},
},
// Version Badge
// Version Badge
...
@@ -1731,6 +1733,7 @@ export default {
...
@@ -1731,6 +1733,7 @@ export default {
expires
:
'
到期时间
'
,
expires
:
'
到期时间
'
,
noExpiration
:
'
无到期时间
'
,
noExpiration
:
'
无到期时间
'
,
unlimited
:
'
无限制
'
,
unlimited
:
'
无限制
'
,
unlimitedDesc
:
'
该订阅无用量限制
'
,
daily
:
'
每日
'
,
daily
:
'
每日
'
,
weekly
:
'
每周
'
,
weekly
:
'
每周
'
,
monthly
:
'
每月
'
,
monthly
:
'
每月
'
,
...
...
frontend/src/views/admin/SubscriptionsView.vue
View file @
2ccdc2b8
...
@@ -202,16 +202,19 @@
...
@@ -202,16 +202,19 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
No
Limits
-->
<!--
No
Limits
-
Unlimited
badge
-->
<
div
<
div
v
-
if
=
"
v
-
if
=
"
!row.group?.daily_limit_usd &&
!row.group?.daily_limit_usd &&
!row.group?.weekly_limit_usd &&
!row.group?.weekly_limit_usd &&
!row.group?.monthly_limit_usd
!row.group?.monthly_limit_usd
"
"
class
=
"
text-xs text-gray-50
0
"
class
=
"
flex items-center gap-2 rounded-lg bg-gradient-to-r from-emerald-50 to-teal-50 px-3 py-2 dark:from-emerald-900/20 dark:to-teal-900/2
0
"
>
>
{{
t
(
'
admin.subscriptions.noLimits
'
)
}}
<
span
class
=
"
text-lg text-emerald-600 dark:text-emerald-400
"
>
∞
<
/span
>
<
span
class
=
"
text-xs font-medium text-emerald-700 dark:text-emerald-300
"
>
{{
t
(
'
admin.subscriptions.unlimited
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
<
/template
>
...
...
frontend/src/views/user/SubscriptionsView.vue
View file @
2ccdc2b8
...
@@ -230,18 +230,26 @@
...
@@ -230,18 +230,26 @@
<
/p
>
<
/p
>
<
/div
>
<
/div
>
<!--
No
limits
configured
-->
<!--
No
limits
configured
-
Unlimited
badge
-->
<
div
<
div
v
-
if
=
"
v
-
if
=
"
!subscription.group?.daily_limit_usd &&
!subscription.group?.daily_limit_usd &&
!subscription.group?.weekly_limit_usd &&
!subscription.group?.weekly_limit_usd &&
!subscription.group?.monthly_limit_usd
!subscription.group?.monthly_limit_usd
"
"
class
=
"
py-4 text-center
"
class
=
"
flex items-center justify-center rounded-xl bg-gradient-to-r from-emerald-50 to-teal-50 py-6 dark:from-emerald-900/20 dark:to-teal-900/20
"
>
>
<
span
class
=
"
text-sm text-gray-500 dark:text-dark-400
"
>
{{
<
div
class
=
"
flex items-center gap-3
"
>
t
(
'
userSubscriptions.unlimited
'
)
<
span
class
=
"
text-4xl text-emerald-600 dark:text-emerald-400
"
>
∞
<
/span
>
}}
<
/span
>
<
div
>
<
p
class
=
"
text-sm font-medium text-emerald-700 dark:text-emerald-300
"
>
{{
t
(
'
userSubscriptions.unlimited
'
)
}}
<
/p
>
<
p
class
=
"
text-xs text-emerald-600/70 dark:text-emerald-400/70
"
>
{{
t
(
'
userSubscriptions.unlimitedDesc
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment