80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
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 }
|
|
})
|