Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -10,15 +10,17 @@ import (
|
|||||||
|
|
||||||
// SettingHandler 系统设置处理器
|
// SettingHandler 系统设置处理器
|
||||||
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) {
|
|||||||
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,
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
|||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,94 +69,108 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Progress bars -->
|
<!-- Progress bars or Unlimited badge -->
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<div v-if="subscription.group?.daily_limit_usd" class="flex items-center gap-2">
|
<!-- Unlimited subscription badge -->
|
||||||
<span class="w-8 flex-shrink-0 text-[10px] text-gray-500">{{
|
<div
|
||||||
t('subscriptionProgress.daily')
|
v-if="isUnlimited(subscription)"
|
||||||
}}</span>
|
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"
|
||||||
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
>
|
||||||
<div
|
<span class="text-lg text-emerald-600 dark:text-emerald-400">∞</span>
|
||||||
class="h-1.5 rounded-full transition-all"
|
<span class="text-xs font-medium text-emerald-700 dark:text-emerald-300">
|
||||||
:class="
|
{{ t('subscriptionProgress.unlimited') }}
|
||||||
getProgressBarClass(
|
|
||||||
subscription.daily_usage_usd,
|
|
||||||
subscription.group?.daily_limit_usd
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:style="{
|
|
||||||
width: getProgressWidth(
|
|
||||||
subscription.daily_usage_usd,
|
|
||||||
subscription.group?.daily_limit_usd
|
|
||||||
)
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
|
||||||
{{
|
|
||||||
formatUsage(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="subscription.group?.weekly_limit_usd" class="flex items-center gap-2">
|
<!-- Progress bars for limited subscriptions -->
|
||||||
<span class="w-8 flex-shrink-0 text-[10px] text-gray-500">{{
|
<template v-else>
|
||||||
t('subscriptionProgress.weekly')
|
<div v-if="subscription.group?.daily_limit_usd" class="flex items-center gap-2">
|
||||||
}}</span>
|
<span class="w-8 flex-shrink-0 text-[10px] text-gray-500">{{
|
||||||
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
t('subscriptionProgress.daily')
|
||||||
<div
|
}}</span>
|
||||||
class="h-1.5 rounded-full transition-all"
|
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
||||||
:class="
|
<div
|
||||||
getProgressBarClass(
|
class="h-1.5 rounded-full transition-all"
|
||||||
subscription.weekly_usage_usd,
|
:class="
|
||||||
subscription.group?.weekly_limit_usd
|
getProgressBarClass(
|
||||||
)
|
subscription.daily_usage_usd,
|
||||||
"
|
subscription.group?.daily_limit_usd
|
||||||
:style="{
|
)
|
||||||
width: getProgressWidth(
|
"
|
||||||
subscription.weekly_usage_usd,
|
:style="{
|
||||||
subscription.group?.weekly_limit_usd
|
width: getProgressWidth(
|
||||||
)
|
subscription.daily_usage_usd,
|
||||||
}"
|
subscription.group?.daily_limit_usd
|
||||||
></div>
|
)
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
||||||
|
{{
|
||||||
|
formatUsage(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
|
||||||
{{
|
|
||||||
formatUsage(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="subscription.group?.monthly_limit_usd" class="flex items-center gap-2">
|
<div v-if="subscription.group?.weekly_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.monthly')
|
t('subscriptionProgress.weekly')
|
||||||
}}</span>
|
}}</span>
|
||||||
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
||||||
<div
|
<div
|
||||||
class="h-1.5 rounded-full transition-all"
|
class="h-1.5 rounded-full transition-all"
|
||||||
:class="
|
:class="
|
||||||
getProgressBarClass(
|
getProgressBarClass(
|
||||||
subscription.monthly_usage_usd,
|
subscription.weekly_usage_usd,
|
||||||
subscription.group?.monthly_limit_usd
|
subscription.group?.weekly_limit_usd
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:style="{
|
:style="{
|
||||||
width: getProgressWidth(
|
width: getProgressWidth(
|
||||||
subscription.monthly_usage_usd,
|
subscription.weekly_usage_usd,
|
||||||
subscription.group?.monthly_limit_usd
|
subscription.group?.weekly_limit_usd
|
||||||
)
|
)
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
||||||
|
{{
|
||||||
|
formatUsage(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
|
||||||
{{
|
<div v-if="subscription.group?.monthly_limit_usd" class="flex items-center gap-2">
|
||||||
formatUsage(
|
<span class="w-8 flex-shrink-0 text-[10px] text-gray-500">{{
|
||||||
subscription.monthly_usage_usd,
|
t('subscriptionProgress.monthly')
|
||||||
subscription.group?.monthly_limit_usd
|
}}</span>
|
||||||
)
|
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
||||||
}}
|
<div
|
||||||
</span>
|
class="h-1.5 rounded-full transition-all"
|
||||||
</div>
|
:class="
|
||||||
|
getProgressBarClass(
|
||||||
|
subscription.monthly_usage_usd,
|
||||||
|
subscription.group?.monthly_limit_usd
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:style="{
|
||||||
|
width: getProgressWidth(
|
||||||
|
subscription.monthly_usage_usd,
|
||||||
|
subscription.group?.monthly_limit_usd
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
||||||
|
{{
|
||||||
|
formatUsage(
|
||||||
|
subscription.monthly_usage_usd,
|
||||||
|
subscription.group?.monthly_limit_usd
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 {
|
|||||||
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 {
|
|||||||
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',
|
||||||
|
|||||||
@@ -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 {
|
|||||||
expiresToday: '今天到期',
|
expiresToday: '今天到期',
|
||||||
expiresTomorrow: '明天到期',
|
expiresTomorrow: '明天到期',
|
||||||
viewAll: '查看全部订阅',
|
viewAll: '查看全部订阅',
|
||||||
noSubscriptions: '暂无有效订阅'
|
noSubscriptions: '暂无有效订阅',
|
||||||
|
unlimited: '无限制'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Version Badge
|
// Version Badge
|
||||||
@@ -1731,6 +1733,7 @@ export default {
|
|||||||
expires: '到期时间',
|
expires: '到期时间',
|
||||||
noExpiration: '无到期时间',
|
noExpiration: '无到期时间',
|
||||||
unlimited: '无限制',
|
unlimited: '无限制',
|
||||||
|
unlimitedDesc: '该订阅无用量限制',
|
||||||
daily: '每日',
|
daily: '每日',
|
||||||
weekly: '每周',
|
weekly: '每周',
|
||||||
monthly: '每月',
|
monthly: '每月',
|
||||||
|
|||||||
@@ -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-500"
|
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/20"
|
||||||
>
|
>
|
||||||
{{ 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user