feat(server): enhance config guard with custom error messages and add FLUX_PACKAGES handling

This commit is contained in:
RainbowBird
2026-03-28 02:25:44 +08:00
committed by RainbowBird
parent a70001f461
commit a64446fbb7
8 changed files with 90 additions and 13 deletions
+6 -2
View File
@@ -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<ConfigKVService['get']>[0][]): MiddlewareHandler<HonoEnv> {
export function configGuard(
configKV: ConfigKVService,
keys: Parameters<ConfigKVService['get']>[0][],
message = 'Service is not available yet',
): MiddlewareHandler<HonoEnv> {
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()
}
+5 -1
View File
@@ -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<HonoEnv>()
.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')
+1 -1
View File
@@ -54,7 +54,7 @@ export function createV1CompletionsRoutes(fluxService: FluxService, configKV: Co
}
return new Hono<HonoEnv>()
.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)
}
@@ -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()
})
})
+24 -2
View File
@@ -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<K extends keyof ConfigDefinitions>(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<K extends keyof ConfigDefinitions>(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<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K] | null> {
@@ -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<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
@@ -29,7 +51,7 @@ export function createConfigKVService(redis: Redis) {
},
async set<K extends keyof ConfigDefinitions>(key: K, value: ConfigDefinitions[K]): Promise<void> {
await redis.set(`${KEY_PREFIX}${key}`, String(value))
await redis.set(`${KEY_PREFIX}${key}`, serializeValue(key, value))
},
}
}