From d07b159c18d3ebe2fa507661471ec2c217197a4b Mon Sep 17 00:00:00 2001 From: CaIon Date: Tue, 12 May 2026 16:43:32 +0800 Subject: [PATCH 1/2] fix: theme-aware payment paths, auto-group validation, route guards, perf group filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add common.ThemeAwarePath to generate correct redirect URLs based on active theme (default vs classic), replacing hardcoded /console/* paths in 7 controllers and service/quota.go (#4765) - Validate auto-group availability against getUserGroups before defaulting form values; playground falls back to 'default' group when 'auto' is unavailable (#4796, #4799) - Enforce HeaderNavModules settings in rankings route (frontend + backend API) and SidebarModulesAdmin in playground route to block direct URL access when features are disabled (#4704, #4512) - Filter perf_metrics API response to only include currently configured groups, hiding stale data from deleted groups (#4790) - Preserve query params (pay=success/fail) in /console/topup → /wallet frontend redirect --- common/constants.go | 21 +++++ controller/perf_metrics.go | 14 ++++ controller/rankings.go | 40 ++++++++++ controller/return_path.go | 13 +++ controller/subscription_payment_epay.go | 15 ++-- controller/subscription_payment_stripe.go | 5 +- controller/telegram.go | 2 +- controller/topup.go | 3 +- controller/topup_stripe.go | 5 +- controller/topup_waffo.go | 3 +- controller/topup_waffo_pancake.go | 3 +- service/quota.go | 5 +- service/return_path.go | 13 +++ .../components/api-keys-mutate-drawer.tsx | 29 ++++--- .../src/features/playground/constants.ts | 5 +- web/default/src/features/playground/index.tsx | 27 +++---- web/default/src/lib/nav-modules.ts | 80 +++++++++++++++++++ .../_authenticated/playground/index.tsx | 8 +- web/default/src/routes/console/topup.tsx | 8 +- web/default/src/routes/rankings/index.tsx | 16 +++- 20 files changed, 255 insertions(+), 60 deletions(-) create mode 100644 controller/return_path.go create mode 100644 service/return_path.go create mode 100644 web/default/src/lib/nav-modules.ts diff --git a/common/constants.go b/common/constants.go index c4d2511ef35..1912e4602ae 100644 --- a/common/constants.go +++ b/common/constants.go @@ -4,6 +4,7 @@ import ( "crypto/tls" //"os" //"strconv" + "strings" "sync" "sync/atomic" "time" @@ -36,6 +37,26 @@ func SetTheme(t string) { } } +// ThemeAwarePath rewrites legacy /console/* paths to the default-theme +// equivalents when the active theme is "default". For "classic" (or any +// other theme) the path is returned unchanged. The function only touches +// known prefixes so it is safe to call with arbitrary suffixes and query +// strings. +func ThemeAwarePath(suffix string) string { + if GetTheme() != "default" { + return suffix + } + switch { + case strings.HasPrefix(suffix, "/console/topup"): + return strings.Replace(suffix, "/console/topup", "/wallet", 1) + case strings.HasPrefix(suffix, "/console/log"): + return strings.Replace(suffix, "/console/log", "/usage-logs", 1) + case strings.HasPrefix(suffix, "/console/personal"): + return strings.Replace(suffix, "/console/personal", "/profile", 1) + } + return suffix +} + // var ChatLink = "" // var ChatLink2 = "" var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens diff --git a/controller/perf_metrics.go b/controller/perf_metrics.go index 2ffc2b91383..51e8d9ecda2 100644 --- a/controller/perf_metrics.go +++ b/controller/perf_metrics.go @@ -5,6 +5,7 @@ import ( "strconv" perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics" + "github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) @@ -62,8 +63,21 @@ func GetPerfMetrics(c *gin.Context) { return } + result.Groups = filterActiveGroups(result.Groups) + c.JSON(http.StatusOK, gin.H{ "success": true, "data": result, }) } + +func filterActiveGroups(groups []perfmetrics.GroupResult) []perfmetrics.GroupResult { + activeGroups := ratio_setting.GetGroupRatioCopy() + filtered := make([]perfmetrics.GroupResult, 0, len(groups)) + for _, g := range groups { + if _, ok := activeGroups[g.Group]; ok || g.Group == "auto" { + filtered = append(filtered, g) + } + } + return filtered +} diff --git a/controller/rankings.go b/controller/rankings.go index 5a7fdaae16b..a3fdf2b565b 100644 --- a/controller/rankings.go +++ b/controller/rankings.go @@ -3,11 +3,51 @@ package controller import ( "net/http" + "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" ) +func isRankingsEnabled() bool { + common.OptionMapRWMutex.RLock() + raw := common.OptionMap["HeaderNavModules"] + common.OptionMapRWMutex.RUnlock() + + if raw == "" { + return true + } + + var parsed map[string]interface{} + if err := common.Unmarshal([]byte(raw), &parsed); err != nil { + return true + } + rankings, ok := parsed["rankings"] + if !ok { + return true + } + switch v := rankings.(type) { + case bool: + return v + case map[string]interface{}: + if enabled, ok := v["enabled"]; ok { + if b, ok := enabled.(bool); ok { + return b + } + } + return true + } + return true +} + func GetRankings(c *gin.Context) { + if !isRankingsEnabled() { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "rankings is disabled", + }) + return + } + result, err := service.GetRankingsSnapshot(c.DefaultQuery("period", "week")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ diff --git a/controller/return_path.go b/controller/return_path.go new file mode 100644 index 00000000000..28378b7e5fe --- /dev/null +++ b/controller/return_path.go @@ -0,0 +1,13 @@ +package controller + +import ( + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/system_setting" +) + +func paymentReturnPath(suffix string) string { + base := strings.TrimRight(system_setting.ServerAddress, "/") + return base + common.ThemeAwarePath(suffix) +} diff --git a/controller/subscription_payment_epay.go b/controller/subscription_payment_epay.go index 2567654ff47..ecd38472861 100644 --- a/controller/subscription_payment_epay.go +++ b/controller/subscription_payment_epay.go @@ -12,7 +12,6 @@ import ( "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" "github.com/samber/lo" ) @@ -173,7 +172,7 @@ func SubscriptionEpayReturn(c *gin.Context) { if c.Request.Method == "POST" { // POST 请求:从 POST body 解析参数 if err := c.Request.ParseForm(); err != nil { - c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail") + c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail")) return } params = lo.Reduce(lo.Keys(c.Request.PostForm), func(r map[string]string, t string, i int) map[string]string { @@ -189,29 +188,29 @@ func SubscriptionEpayReturn(c *gin.Context) { } if len(params) == 0 { - c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail") + c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail")) return } client := GetEpayClient() if client == nil { - c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail") + c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail")) return } verifyInfo, err := client.Verify(params) if err != nil || !verifyInfo.VerifyStatus { - c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail") + c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail")) return } if verifyInfo.TradeStatus == epay.StatusTradeSuccess { LockOrder(verifyInfo.ServiceTradeNo) defer UnlockOrder(verifyInfo.ServiceTradeNo) if err := model.CompleteSubscriptionOrder(verifyInfo.ServiceTradeNo, common.GetJsonString(verifyInfo), model.PaymentProviderEpay, verifyInfo.Type); err != nil { - c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail") + c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=fail")) return } - c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=success") + c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=success")) return } - c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=pending") + c.Redirect(http.StatusFound, paymentReturnPath("/console/topup?pay=pending")) } diff --git a/controller/subscription_payment_stripe.go b/controller/subscription_payment_stripe.go index a5ce4685b4c..3efe7b38f31 100644 --- a/controller/subscription_payment_stripe.go +++ b/controller/subscription_payment_stripe.go @@ -10,7 +10,6 @@ import ( "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting" - "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" "github.com/stripe/stripe-go/v81" "github.com/stripe/stripe-go/v81/checkout/session" @@ -111,8 +110,8 @@ func genStripeSubscriptionLink(referenceId string, customerId string, email stri params := &stripe.CheckoutSessionParams{ ClientReferenceID: stripe.String(referenceId), - SuccessURL: stripe.String(system_setting.ServerAddress + "/console/topup"), - CancelURL: stripe.String(system_setting.ServerAddress + "/console/topup"), + SuccessURL: stripe.String(paymentReturnPath("/console/topup")), + CancelURL: stripe.String(paymentReturnPath("/console/topup")), LineItems: []*stripe.CheckoutSessionLineItemParams{ { Price: stripe.String(priceId), diff --git a/controller/telegram.go b/controller/telegram.go index f16cdd66c54..b5918d8e0ed 100644 --- a/controller/telegram.go +++ b/controller/telegram.go @@ -66,7 +66,7 @@ func TelegramBind(c *gin.Context) { return } - c.Redirect(302, "/console/personal") + c.Redirect(302, common.ThemeAwarePath("/console/personal")) } func TelegramLogin(c *gin.Context) { diff --git a/controller/topup.go b/controller/topup.go index f2848671433..208de2c7ead 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -14,7 +14,6 @@ import ( "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/setting/system_setting" "github.com/Calcium-Ion/go-epay/epay" "github.com/gin-gonic/gin" @@ -208,7 +207,7 @@ func RequestEpay(c *gin.Context) { } callBackAddress := service.GetCallbackAddress() - returnUrl, _ := url.Parse(system_setting.ServerAddress + "/console/log") + returnUrl, _ := url.Parse(paymentReturnPath("/console/log")) notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify") tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix()) tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo) diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index ceee8ecdd66..bcae201ec4f 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -15,7 +15,6 @@ import ( "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" "github.com/stripe/stripe-go/v81" @@ -348,10 +347,10 @@ func genStripeLink(referenceId string, customerId string, email string, amount i // Use custom URLs if provided, otherwise use defaults if successURL == "" { - successURL = system_setting.ServerAddress + "/console/log" + successURL = paymentReturnPath("/console/log") } if cancelURL == "" { - cancelURL = system_setting.ServerAddress + "/console/topup" + cancelURL = paymentReturnPath("/console/topup") } params := &stripe.CheckoutSessionParams{ diff --git a/controller/topup_waffo.go b/controller/topup_waffo.go index 1885c1ded9e..344630f7421 100644 --- a/controller/topup_waffo.go +++ b/controller/topup_waffo.go @@ -14,7 +14,6 @@ import ( "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" "github.com/thanhpk/randstr" waffo "github.com/waffo-com/waffo-go" @@ -237,7 +236,7 @@ func RequestWaffoPay(c *gin.Context) { if setting.WaffoNotifyUrl != "" { notifyUrl = setting.WaffoNotifyUrl } - returnUrl := system_setting.ServerAddress + "/console/topup?show_history=true" + returnUrl := paymentReturnPath("/console/topup?show_history=true") if setting.WaffoReturnUrl != "" { returnUrl = setting.WaffoReturnUrl } diff --git a/controller/topup_waffo_pancake.go b/controller/topup_waffo_pancake.go index 09f1516304b..11c581fa699 100644 --- a/controller/topup_waffo_pancake.go +++ b/controller/topup_waffo_pancake.go @@ -13,7 +13,6 @@ import ( "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" - "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" "github.com/thanhpk/randstr" @@ -107,7 +106,7 @@ func getWaffoPancakeReturnURL() string { if strings.TrimSpace(setting.WaffoPancakeReturnURL) != "" { return setting.WaffoPancakeReturnURL } - return strings.TrimRight(system_setting.ServerAddress, "/") + "/console/topup?show_history=true" + return paymentReturnPath("/console/topup?show_history=true") } func RequestWaffoPancakePay(c *gin.Context) { diff --git a/service/quota.go b/service/quota.go index 7364598a5de..e2ab25cf09b 100644 --- a/service/quota.go +++ b/service/quota.go @@ -17,7 +17,6 @@ import ( perfmetrics "github.com/QuantumNous/new-api/pkg/perf_metrics" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/setting/ratio_setting" - "github.com/QuantumNous/new-api/setting/system_setting" "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" @@ -467,7 +466,7 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon } if quotaTooLow { prompt := "您的额度即将用尽" - topUpLink := fmt.Sprintf("%s/console/topup", system_setting.ServerAddress) + topUpLink := PaymentReturnURL("/console/topup") // 根据通知方式生成不同的内容格式 var content string @@ -521,7 +520,7 @@ func checkAndSendSubscriptionQuotaNotify(relayInfo *relaycommon.RelayInfo) { } prompt := "您的订阅额度即将用尽" - topUpLink := fmt.Sprintf("%s/console/topup", system_setting.ServerAddress) + topUpLink := PaymentReturnURL("/console/topup") var content string var values []interface{} diff --git a/service/return_path.go b/service/return_path.go new file mode 100644 index 00000000000..c99e1fd3400 --- /dev/null +++ b/service/return_path.go @@ -0,0 +1,13 @@ +package service + +import ( + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/system_setting" +) + +func PaymentReturnURL(suffix string) string { + base := strings.TrimRight(system_setting.ServerAddress, "/") + return base + common.ThemeAwarePath(suffix) +} diff --git a/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx b/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx index c640a986af9..ed128e574b3 100644 --- a/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx +++ b/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx @@ -151,15 +151,7 @@ export function ApiKeysMutateDrawer({ ratio: info.ratio, }) ) - - // Add auto group if configured - if (!groups.some((g) => g.value === 'auto')) { - groups.unshift({ - value: 'auto', - label: 'auto', - desc: t('Auto (Circuit Breaker)'), - }) - } + const backendHasAuto = groups.some((g) => g.value === 'auto') const form = useForm({ resolver: zodResolver(apiKeyFormSchema), @@ -169,17 +161,28 @@ export function ApiKeysMutateDrawer({ // Load existing data when updating useEffect(() => { if (open && isUpdate && currentRow) { - // For update, fetch fresh data getApiKey(currentRow.id).then((result) => { if (result.success && result.data) { form.reset(transformApiKeyToFormDefaults(result.data)) } }) } else if (open && !isUpdate) { - // For create, reset to defaults - form.reset(getApiKeyFormDefaultValues(defaultUseAutoGroup)) + form.reset(getApiKeyFormDefaultValues(defaultUseAutoGroup && backendHasAuto)) + } + }, [open, isUpdate, currentRow, form, defaultUseAutoGroup, backendHasAuto]) + + // Correct group after groups load: if the form value is not in available groups, fall back + useEffect(() => { + if (groups.length === 0) return + const currentGroup = form.getValues('group') + if (currentGroup && !groups.some((g) => g.value === currentGroup)) { + const fallback = groups.find((g) => g.value === 'default')?.value ?? groups[0]?.value ?? '' + form.setValue('group', fallback) + if (currentGroup === 'auto') { + form.setValue('cross_group_retry', false) + } } - }, [open, isUpdate, currentRow, form, defaultUseAutoGroup]) + }, [groups, form]) const onSubmit = async (data: ApiKeyFormValues) => { setIsSubmitting(true) diff --git a/web/default/src/features/playground/constants.ts b/web/default/src/features/playground/constants.ts index 8748a70425c..cd2ef913739 100644 --- a/web/default/src/features/playground/constants.ts +++ b/web/default/src/features/playground/constants.ts @@ -39,8 +39,9 @@ export const API_ENDPOINTS = { USER_GROUPS: '/api/user/self/groups', } as const -// Default group -export const DEFAULT_GROUP = 'auto' as const +// Default group — uses 'default' as the safe fallback; auto-group is +// only selected when the backend confirms it is available for the user. +export const DEFAULT_GROUP = 'default' as const // Default configuration export const DEFAULT_CONFIG: PlaygroundConfig = { diff --git a/web/default/src/features/playground/index.tsx b/web/default/src/features/playground/index.tsx index 49d4a37c56b..a9a86b76b56 100644 --- a/web/default/src/features/playground/index.tsx +++ b/web/default/src/features/playground/index.tsx @@ -21,7 +21,6 @@ import { useQuery } from '@tanstack/react-query' import { getUserModels, getUserGroups } from './api' import { PlaygroundChat } from './components/playground-chat' import { PlaygroundInput } from './components/playground-input' -import { DEFAULT_GROUP } from './constants' import { usePlaygroundState, useChatHandler } from './hooks' import { createUserMessage, createLoadingAssistantMessage } from './lib' import type { Message as MessageType } from './types' @@ -79,22 +78,16 @@ export function Playground() { useEffect(() => { if (!groupsData) return - // Add auto group if not present - const hasAutoGroup = groupsData.some((g) => g.value === DEFAULT_GROUP) - const processedGroups = hasAutoGroup - ? groupsData - : [ - { - value: DEFAULT_GROUP, - label: 'Auto', - ratio: 1, - desc: 'Circuit Breaker', - }, - ...groupsData, - ] - - setGroups(processedGroups) - }, [groupsData, setGroups]) + setGroups(groupsData) + + const hasCurrentGroup = groupsData.some((g) => g.value === config.group) + if (!hasCurrentGroup && groupsData.length > 0) { + const fallback = + groupsData.find((g) => g.value === 'default')?.value ?? + groupsData[0].value + updateConfig('group', fallback) + } + }, [groupsData, setGroups, config.group, updateConfig]) const handleSendMessage = (text: string) => { const userMessage = createUserMessage(text) diff --git a/web/default/src/lib/nav-modules.ts b/web/default/src/lib/nav-modules.ts new file mode 100644 index 00000000000..93ab5388014 --- /dev/null +++ b/web/default/src/lib/nav-modules.ts @@ -0,0 +1,80 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +type ModuleAccess = { enabled: boolean; requireAuth: boolean } + +const DEFAULTS: Record = { + pricing: { enabled: true, requireAuth: false }, + rankings: { enabled: true, requireAuth: false }, +} + +function parseAccess(raw: unknown, fallback: ModuleAccess): ModuleAccess { + if (typeof raw === 'boolean') return { enabled: raw, requireAuth: fallback.requireAuth } + if (raw && typeof raw === 'object') { + const r = raw as Record + return { + enabled: typeof r.enabled === 'boolean' ? r.enabled : fallback.enabled, + requireAuth: typeof r.requireAuth === 'boolean' ? r.requireAuth : fallback.requireAuth, + } + } + return { ...fallback } +} + +function getCachedStatus(): Record | null { + try { + const raw = window.localStorage.getItem('status') + return raw ? (JSON.parse(raw) as Record) : null + } catch { + return null + } +} + +export function getModuleAccess(module: 'rankings' | 'pricing'): ModuleAccess { + const status = getCachedStatus() + if (!status) return DEFAULTS[module] + + const rawNav = status.HeaderNavModules + if (!rawNav || String(rawNav).trim() === '') return DEFAULTS[module] + + try { + const parsed = JSON.parse(String(rawNav)) as Record + return parseAccess(parsed[module], DEFAULTS[module]) + } catch { + return DEFAULTS[module] + } +} + +export function isSidebarModuleEnabled(section: string, module: string): boolean { + const status = getCachedStatus() + if (!status) return true + + const raw = status.SidebarModulesAdmin + if (!raw || String(raw).trim() === '') return true + + try { + const parsed = JSON.parse(String(raw)) as Record> + const sectionConfig = parsed[section] + if (!sectionConfig) return true + if (sectionConfig.enabled === false) return false + if (sectionConfig[module] === false) return false + return true + } catch { + return true + } +} diff --git a/web/default/src/routes/_authenticated/playground/index.tsx b/web/default/src/routes/_authenticated/playground/index.tsx index 41b5778f3b1..a755accc2e6 100644 --- a/web/default/src/routes/_authenticated/playground/index.tsx +++ b/web/default/src/routes/_authenticated/playground/index.tsx @@ -16,11 +16,17 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' import { Main } from '@/components/layout' import { Playground } from '@/features/playground' +import { isSidebarModuleEnabled } from '@/lib/nav-modules' export const Route = createFileRoute('/_authenticated/playground/')({ + beforeLoad: () => { + if (!isSidebarModuleEnabled('chat', 'playground')) { + throw redirect({ to: '/dashboard' }) + } + }, component: PlaygroundPage, }) diff --git a/web/default/src/routes/console/topup.tsx b/web/default/src/routes/console/topup.tsx index fce5e772812..8e728a2c226 100644 --- a/web/default/src/routes/console/topup.tsx +++ b/web/default/src/routes/console/topup.tsx @@ -16,13 +16,17 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ +import z from 'zod' import { createFileRoute, redirect } from '@tanstack/react-router' +const topupSearchSchema = z.record(z.string(), z.unknown()).catch({}) + export const Route = createFileRoute('/console/topup')({ - beforeLoad: () => { + validateSearch: topupSearchSchema, + beforeLoad: ({ search }) => { throw redirect({ to: '/wallet', - search: { show_history: true }, + search: { show_history: true, ...search }, }) }, }) diff --git a/web/default/src/routes/rankings/index.tsx b/web/default/src/routes/rankings/index.tsx index 05ab20b7314..24de1050dcf 100644 --- a/web/default/src/routes/rankings/index.tsx +++ b/web/default/src/routes/rankings/index.tsx @@ -17,8 +17,10 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import z from 'zod' -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, redirect } from '@tanstack/react-router' import { Rankings } from '@/features/rankings' +import { getModuleAccess } from '@/lib/nav-modules' +import { useAuthStore } from '@/stores/auth-store' const rankingsSearchSchema = z.object({ period: z @@ -29,5 +31,17 @@ const rankingsSearchSchema = z.object({ export const Route = createFileRoute('/rankings/')({ validateSearch: rankingsSearchSchema, + beforeLoad: () => { + const access = getModuleAccess('rankings') + if (!access.enabled) { + throw redirect({ to: '/' }) + } + if (access.requireAuth) { + const { auth } = useAuthStore.getState() + if (!auth.user) { + throw redirect({ to: '/sign-in', search: { redirect: '/rankings' } }) + } + } + }, component: Rankings, }) From 35f6880ad2e6c4e413a327800a07c1a15ca613c3 Mon Sep 17 00:00:00 2001 From: CaIon Date: Tue, 12 May 2026 16:43:56 +0800 Subject: [PATCH 2/2] fix: update hero section text and localization strings for clarity --- web/default/src/features/home/components/sections/hero.tsx | 7 +------ web/default/src/i18n/locales/en.json | 2 +- web/default/src/i18n/locales/fr.json | 2 +- web/default/src/i18n/locales/ja.json | 2 +- web/default/src/i18n/locales/ru.json | 2 +- web/default/src/i18n/locales/vi.json | 2 +- web/default/src/i18n/locales/zh.json | 2 +- 7 files changed, 7 insertions(+), 12 deletions(-) diff --git a/web/default/src/features/home/components/sections/hero.tsx b/web/default/src/features/home/components/sections/hero.tsx index f5955972450..34f6d915042 100644 --- a/web/default/src/features/home/components/sections/hero.tsx +++ b/web/default/src/features/home/components/sections/hero.tsx @@ -19,7 +19,6 @@ For commercial licensing, please contact support@quantumnous.com import { Link } from '@tanstack/react-router' import { ArrowRight } from 'lucide-react' import { useTranslation } from 'react-i18next' -import { useSystemConfig } from '@/hooks/use-system-config' import { Button } from '@/components/ui/button' import { HeroTerminalDemo } from '../hero-terminal-demo' @@ -30,7 +29,6 @@ interface HeroProps { export function Hero(props: HeroProps) { const { t } = useTranslation() - const { systemName } = useSystemConfig() return (
@@ -67,10 +65,7 @@ export function Hero(props: HeroProps) { className='landing-animate-fade-up text-muted-foreground/80 mt-5 max-w-lg text-base leading-relaxed opacity-0 md:text-lg' style={{ animationDelay: '80ms' }} > - {systemName}{' '} - {t( - 'is an open-source AI API gateway for self-hosted deployments. Connect multiple upstream services, manage models, keys, quotas, logs, and routing policies in one place.' - )} + {t('Power AI applications, manage digital assets, connect the Future')}