feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
import { createApiError } from '~~/server/utils/apiError'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
import { getClientIP } from '~~/server/utils/ip-utils'
import { checkDistributedRateLimit } from '~~/server/utils/rateLimiter'
import { getServerTimestamp } from '~~/server/utils/serverTime'
import { SmtpService } from '~~/server/services/smtpService'
import { db } from '~/drizzle/db'
import { systemSettings } from '~/drizzle/schema'
import {
generateEmailCode,
storeEmailCode,
isEmailCodeCooldownActive,
isEmailCodeDailyLimitReached,
cleanupExpiredEmailCodes
} from '~~/server/utils/email-verification'
const EMAIL_CODE_IP_LIMIT = 10
const EMAIL_CODE_IP_WINDOW_MS = 60 * 1000
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const email = typeof body.email === 'string' ? body.email.trim() : ''
const clientIp = getClientIP(event)
if (!email || !EMAIL_REGEX.test(email)) {
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '请输入有效的邮箱地址')
}
// 注册/三方注册任一开放时允许发码(否则拒绝,避免成为邮件群发通道)
const config = await db.query.systemSettings.findFirst()
if (!config?.allowRegister && !config?.allowOAuthRegistration) {
throw createApiError(403, SERVER_ERROR_CODES.AUTH_REGISTER_DISABLED, '系统未开放注册')
}
// IP 限流:每分钟最多 10 次
const limitResult = await checkDistributedRateLimit(`email-code:${clientIp}`, EMAIL_CODE_IP_LIMIT, EMAIL_CODE_IP_WINDOW_MS)
if (!limitResult.isAllowed) {
const waitSeconds = Math.ceil((limitResult.resetTime - getServerTimestamp()) / 1000)
throw createApiError(429, SERVER_ERROR_CODES.AUTH_RATE_LIMITED_SECONDS, `操作过于频繁,请 ${waitSeconds} 秒后再试`, { params: [waitSeconds] })
}
// 同邮箱 60 秒冷却
if (await isEmailCodeCooldownActive(email)) {
throw createApiError(429, SERVER_ERROR_CODES.AUTH_EMAIL_CODE_COOLDOWN, '验证码发送过于频繁,请 1 分钟后再试')
}
// 同一邮箱每日发送上限
if (await isEmailCodeDailyLimitReached(email)) {
throw createApiError(429, SERVER_ERROR_CODES.AUTH_EMAIL_CODE_DAILY_LIMIT, '今日验证码发送次数已达上限,请明天再试')
}
// 邮件服务可用性检查
const smtpService = SmtpService.getInstance()
if (!(await smtpService.ensureInitialized())) {
throw createApiError(503, SERVER_ERROR_CODES.AUTH_EMAIL_SERVICE_UNAVAILABLE, '邮件服务未配置或不可用,请联系管理员')
}
// 先发邮件,发送成功后才落码(失败不落码,也不触发冷却)
const code = generateEmailCode()
const sent = await smtpService.renderAndSend(email, 'verification.code', {
title: 'VoiceHub 注册邮箱验证',
// 补 builtin verification 模板所需变量(code 必填)
name: '用户',
email,
code,
expiresInMinutes: 5
})
if (!sent) {
throw createApiError(503, SERVER_ERROR_CODES.AUTH_EMAIL_SERVICE_UNAVAILABLE, '验证码邮件发送失败,请稍后重试')
}
cleanupExpiredEmailCodes()
await storeEmailCode(email, code)
return { success: true }
})