Files
BYW-VoiceHub/server/api/card-codes/validate.post.ts
T

107 lines
3.3 KiB
TypeScript

import { db } from '~/drizzle/db'
import { cardCodes } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
import { getSystemSettingsCached } from '~~/server/utils/system-settings-helper'
import { setResponseHeader, type H3Event } from 'h3'
import { getClientIP } from '~~/server/utils/ip-utils'
import { checkDistributedRateLimit } from '~~/server/utils/rateLimiter'
import { getServerTimestamp } from '~~/server/utils/serverTime'
import { createApiError } from '~~/server/utils/apiError'
const USER_BURST_LIMIT = 5
const USER_BURST_WINDOW_MS = 60 * 1000
const USER_HOURLY_LIMIT = 20
const USER_HOURLY_WINDOW_MS = 60 * 60 * 1000
const IP_HOURLY_LIMIT = 100
const IP_HOURLY_WINDOW_MS = 60 * 60 * 1000
const throwRateLimitError = (event: H3Event, resetTime: number): never => {
const retryAfterSeconds = Math.max(1, Math.ceil((resetTime - getServerTimestamp()) / 1000))
const waitText =
retryAfterSeconds >= 60
? `${Math.ceil(retryAfterSeconds / 60)} 分钟`
: `${retryAfterSeconds} 秒`
setResponseHeader(event, 'Retry-After', String(retryAfterSeconds))
throw createError({
statusCode: 429,
message: `点歌券验证请求过于频繁,请等待 ${waitText} 后再试`
})
}
const enforceValidationRateLimit = async (event: H3Event, userId: number) => {
const clientIP = getClientIP(event)
const burstResult = await checkDistributedRateLimit(
`card_code_validate_user_burst:${userId}`,
USER_BURST_LIMIT,
USER_BURST_WINDOW_MS
)
if (!burstResult.isAllowed) {
throwRateLimitError(event, burstResult.resetTime)
}
const hourlyResult = await checkDistributedRateLimit(
`card_code_validate_user_hourly:${userId}`,
USER_HOURLY_LIMIT,
USER_HOURLY_WINDOW_MS
)
if (!hourlyResult.isAllowed) {
throwRateLimitError(event, hourlyResult.resetTime)
}
if (clientIP !== 'unknown') {
const ipResult = await checkDistributedRateLimit(
`card_code_validate_ip_hourly:${clientIP}`,
IP_HOURLY_LIMIT,
IP_HOURLY_WINDOW_MS
)
if (!ipResult.isAllowed) {
throwRateLimitError(event, ipResult.resetTime)
}
}
}
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) {
throw createApiError(401, 'CARD_CODE_AUTH_REQUIRED', 'Sign in to validate request card')
}
await enforceValidationRateLimit(event, user.id)
const body = (await readBody(event)) || {}
const code = typeof body.cardCode === 'string' ? body.cardCode.trim().toUpperCase() : ''
if (!code) {
throw createApiError(400, 'CARD_CODE_REQUIRED', 'Request card is required')
}
const isAdmin = user.role === 'SUPER_ADMIN' || user.role === 'ADMIN'
const settings = await getSystemSettingsCached()
// 点歌券验证仅在启用点歌券时开放
const enabled = settings?.enableCardCodeRequests === true
if (!enabled && !isAdmin) {
throw createApiError(400, 'CARD_CODE_DISABLED', 'Request card submissions are not enabled')
}
const rows = await db
.select({
id: cardCodes.id,
status: cardCodes.status
})
.from(cardCodes)
.where(eq(cardCodes.code, code))
.limit(1)
const found = rows[0]
if (!found || found.status !== 'AVAILABLE') {
throw createApiError(400, 'CARD_CODE_INVALID_OR_USED', 'Request card is invalid or already used')
}
return {
valid: true,
code: 'CARD_CODE_AVAILABLE',
message: 'Request card available'
}
})