107 lines
3.3 KiB
TypeScript
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'
|
|
}
|
|
})
|