From a64446fbb73d01c4c1f19209e01ffbaddafbf1f5 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Wed, 4 Mar 2026 23:19:13 +0800 Subject: [PATCH] feat(server): enhance config guard with custom error messages and add FLUX_PACKAGES handling --- apps/server/src/middlewares/config-guard.ts | 8 +++- apps/server/src/routes/stripe.ts | 6 ++- apps/server/src/routes/v1completions.ts | 2 +- .../src/services/__test__/config-kv.test.ts | 38 +++++++++++++++++++ apps/server/src/services/config-kv.ts | 26 ++++++++++++- packages/i18n/src/locales/en/settings.yaml | 1 + .../i18n/src/locales/zh-Hans/settings.yaml | 1 + .../stage-pages/src/pages/settings/flux.vue | 21 ++++++---- 8 files changed, 90 insertions(+), 13 deletions(-) diff --git a/apps/server/src/middlewares/config-guard.ts b/apps/server/src/middlewares/config-guard.ts index 003282461..d9f55935e 100644 --- a/apps/server/src/middlewares/config-guard.ts +++ b/apps/server/src/middlewares/config-guard.ts @@ -9,12 +9,16 @@ import { createServiceUnavailableError } from '../utils/error' * Middleware factory that checks required config keys exist in Redis. * Returns 503 if any key is missing. */ -export function configGuard(configKV: ConfigKVService, keys: Parameters[0][]): MiddlewareHandler { +export function configGuard( + configKV: ConfigKVService, + keys: Parameters[0][], + message = 'Service is not available yet', +): MiddlewareHandler { return async (_c, next) => { for (const key of keys) { const value = await configKV.getOptional(key) if (value === null) - throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET') + throw createServiceUnavailableError(message, 'CONFIG_NOT_SET') } await next() } diff --git a/apps/server/src/routes/stripe.ts b/apps/server/src/routes/stripe.ts index 91389b9ef..3cbc1018a 100644 --- a/apps/server/src/routes/stripe.ts +++ b/apps/server/src/routes/stripe.ts @@ -20,9 +20,13 @@ const CheckoutBodySchema = object({ export function createStripeRoutes(fluxService: FluxService, stripeService: StripeService, configKV: ConfigKVService, env: Env) { const stripe = env.STRIPE_SECRET_KEY ? new Stripe(env.STRIPE_SECRET_KEY) : null - const fluxConfigGuard = configGuard(configKV, ['FLUX_PER_CENT']) + const fluxConfigGuard = configGuard(configKV, ['FLUX_PER_CENT'], 'Top-up is not available yet') return new Hono() + .get('/packages', async (c) => { + const packages = await configKV.getOptional('FLUX_PACKAGES') + return c.json(packages ?? []) + }) .post('/checkout', authGuard, fluxConfigGuard, async (c) => { if (!stripe) throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED') diff --git a/apps/server/src/routes/v1completions.ts b/apps/server/src/routes/v1completions.ts index 463b8091f..487e9eaa5 100644 --- a/apps/server/src/routes/v1completions.ts +++ b/apps/server/src/routes/v1completions.ts @@ -54,7 +54,7 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co } return new Hono() - .use('*', authGuard, configGuard(configKV, ['FLUX_PER_REQUEST'])) + .use('*', authGuard, configGuard(configKV, ['FLUX_PER_REQUEST'], 'Service is not available yet')) .post('/chat/completions', handleCompletion) .post('/chat/completion', handleCompletion) } diff --git a/apps/server/src/services/__test__/config-kv.test.ts b/apps/server/src/services/__test__/config-kv.test.ts index 1d75cf9cc..db4dea639 100644 --- a/apps/server/src/services/__test__/config-kv.test.ts +++ b/apps/server/src/services/__test__/config-kv.test.ts @@ -71,4 +71,42 @@ describe('configKVService', () => { const value = await service.get('INITIAL_USER_FLUX') expect(value).toBe(500) }) + + // --- FLUX_PACKAGES (JSON) --- + + it('get FLUX_PACKAGES should parse JSON array', async () => { + const packages = [ + { amount: 500, label: '500 Flux', price: '$5' }, + { amount: 1000, label: '1000 Flux', price: '$10' }, + ] + redis._store.set('config:FLUX_PACKAGES', JSON.stringify(packages)) + + const value = await service.get('FLUX_PACKAGES') + expect(value).toEqual(packages) + }) + + it('set FLUX_PACKAGES should serialize as JSON', async () => { + const packages = [{ amount: 500, label: '500 Flux', price: '$5' }] + await service.set('FLUX_PACKAGES', packages) + + const stored = redis._store.get('config:FLUX_PACKAGES') + expect(stored).toBe(JSON.stringify(packages)) + }) + + it('fLUX_PACKAGES round-trip should preserve structure', async () => { + const packages = [ + { amount: 500, label: '500 Flux', price: '$5' }, + { amount: 1000, label: '1000 Flux', price: '$10' }, + { amount: 5000, label: '5000 Flux', price: '$45' }, + ] + await service.set('FLUX_PACKAGES', packages) + + const value = await service.get('FLUX_PACKAGES') + expect(value).toEqual(packages) + }) + + it('getOptional FLUX_PACKAGES should return null when not set', async () => { + const value = await service.getOptional('FLUX_PACKAGES') + expect(value).toBeNull() + }) }) diff --git a/apps/server/src/services/config-kv.ts b/apps/server/src/services/config-kv.ts index d25c9fa35..378a5daba 100644 --- a/apps/server/src/services/config-kv.ts +++ b/apps/server/src/services/config-kv.ts @@ -2,14 +2,36 @@ import type Redis from 'ioredis' import { createServiceUnavailableError } from '../utils/error' +export interface FluxPackage { + /** Amount in cents sent to Stripe */ + amount: number + /** Display label, e.g. "500 Flux" */ + label: string + /** Display price, e.g. "$5" */ + price: string +} + interface ConfigDefinitions { FLUX_PER_CENT: number FLUX_PER_REQUEST: number INITIAL_USER_FLUX: number + FLUX_PACKAGES: FluxPackage[] } const KEY_PREFIX = 'config:' +function parseValue(key: K, raw: string): ConfigDefinitions[K] { + if (key === 'FLUX_PACKAGES') + return JSON.parse(raw) as ConfigDefinitions[K] + return Number(raw) as ConfigDefinitions[K] +} + +function serializeValue(key: K, value: ConfigDefinitions[K]): string { + if (key === 'FLUX_PACKAGES') + return JSON.stringify(value) + return String(value) +} + export function createConfigKVService(redis: Redis) { return { async getOptional(key: K): Promise { @@ -17,7 +39,7 @@ export function createConfigKVService(redis: Redis) { if (raw === null) return null - return Number(raw) as ConfigDefinitions[K] + return parseValue(key, raw) }, async get(key: K): Promise { @@ -29,7 +51,7 @@ export function createConfigKVService(redis: Redis) { }, async set(key: K, value: ConfigDefinitions[K]): Promise { - await redis.set(`${KEY_PREFIX}${key}`, String(value)) + await redis.set(`${KEY_PREFIX}${key}`, serializeValue(key, value)) }, } } diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index 32fa27f94..142144b83 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -487,6 +487,7 @@ pages: title: Flux buy: Charge description: Current Flux + packagesError: Failed to load packages. Please try again later. checkout: success: Payment successful! Your Flux has been topped up. canceled: Payment was canceled. diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index d20e2513c..95863d4d5 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -473,6 +473,7 @@ pages: title: Flux buy: 充能 description: 当前 Flux + packagesError: 加载充值选项失败,请稍后再试。 checkout: success: 支付成功!Flux 已充值。 canceled: 支付已取消。 diff --git a/packages/stage-pages/src/pages/settings/flux.vue b/packages/stage-pages/src/pages/settings/flux.vue index 99973e266..411a1d632 100644 --- a/packages/stage-pages/src/pages/settings/flux.vue +++ b/packages/stage-pages/src/pages/settings/flux.vue @@ -3,7 +3,7 @@ import { client } from '@proj-airi/stage-ui/composables/api' import { useAuthStore } from '@proj-airi/stage-ui/stores/auth' import { Button } from '@proj-airi/ui' import { storeToRefs } from 'pinia' -import { computed, onMounted, ref } from 'vue' +import { onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' @@ -15,15 +15,22 @@ const { credits } = storeToRefs(authStore) const loadingAmount = ref(null) const message = ref<{ type: 'success' | 'error', text: string } | null>(null) +const packages = ref<{ amount: number, label: string, price: string }[]>([]) -// Packages with i18n labels -const packages = computed(() => [ - { amount: 500, label: t('settings.pages.flux.packages.amount_500.label'), price: t('settings.pages.flux.packages.amount_500.price') }, - { amount: 1000, label: t('settings.pages.flux.packages.amount_1000.label'), price: t('settings.pages.flux.packages.amount_1000.price') }, - { amount: 5000, label: t('settings.pages.flux.packages.amount_5000.label'), price: t('settings.pages.flux.packages.amount_5000.price') }, -]) +async function fetchPackages() { + try { + const res = await client.api.stripe.packages.$get() + if (res.ok) + packages.value = await res.json() as { amount: number, label: string, price: string }[] + } + catch { + message.value = { type: 'error', text: t('settings.pages.flux.packagesError') } + } +} onMounted(async () => { + await fetchPackages() + if (route.query.success === 'true') { message.value = { type: 'success', text: t('settings.pages.flux.checkout.success') } await authStore.updateCredits()