import { db } from '~/drizzle/db' import { users } from '~/drizzle/schema' import { eq } from 'drizzle-orm' import { SmtpService } from '~~/server/services/smtpService' import { JWTEnhanced } from '~~/server/utils/jwt-enhanced' import { formatIPForEmail, getClientIP } from '~~/server/utils/ip-utils' import { checkDistributedRateLimit } from '~~/server/utils/rateLimiter' import { getServerTimestamp } from '~~/server/utils/serverTime' import { createApiError } from '~~/server/utils/apiError' export default defineEventHandler(async (event) => { const clientIP = getClientIP(event) // IP 级别限流:每小时最多 5 次请求 const rateLimitKey = `forgot_password_ip:${clientIP}` const limitResult = await checkDistributedRateLimit(rateLimitKey, 5, 60 * 60 * 1000) if (!limitResult.isAllowed) { const waitMinutes = Math.ceil((limitResult.resetTime - getServerTimestamp()) / 60000) throw createApiError(429, 'AUTH_RATE_LIMITED_MINUTES', `操作过于频繁,请等待 ${waitMinutes} 分钟后再试`, { params: [waitMinutes] }) } const body = await readBody(event) const { username, email } = body if (!username) { throw createApiError(400, 'AUTH_ACCOUNT_NAME_REQUIRED', '请提供账号名') } try { const userResult = await db.select().from(users).where(eq(users.username, username)).limit(1) const user = userResult[0] // 如果未提供邮箱,则进入第一步:检查账号并返回掩码邮箱 if (!email) { if (!user) { throw createApiError(404, 'AUTH_ACCOUNT_NAME_NOT_FOUND', '找不到该账号名,请检查是否拼写错误') } if (!user.email) { throw createApiError(400, 'AUTH_NO_SECURITY_EMAIL', '该账号未绑定安全邮箱,无法通过此方式找回密码。请联系管理员。') } // 生成掩码邮箱 (例如: a***b@gmail.com) const emailParts = user.email.split('@') const name = emailParts[0] const domain = emailParts[1] let maskedName = name if (name && name.length > 2) { maskedName = name.substring(0, 1) + '***' + name.substring(name.length - 1) } else if (name && name.length === 2) { maskedName = name.substring(0, 1) + '*' } const maskedEmail = `${maskedName}@${domain}` return { success: true, step: 2, maskedEmail, message: '请输入完整的邮箱地址以验证身份' } } // 无论是否匹配,都返回相同的成功提示 if (user && user.email && user.email.toLowerCase() === email.toLowerCase()) { const payload = { type: 'password_reset', userId: user.id, // 仅截取密码哈希的前10位,防止完整哈希泄露被离线破解 hash: user.password.substring(0, 10) } const token = JWTEnhanced.sign(payload, { expiresIn: '15m' }) // 修复 Host Header Injection 风险,优先使用环境变量中配置的主机名 const config = useRuntimeConfig() let resetLink = '' if (config.public.host) { // 如果配置了主机名,则直接使用 const protocol = config.public.host.startsWith('http') ? '' : process.env.NODE_ENV === 'production' ? 'https://' : 'http://' resetLink = `${protocol}${config.public.host}/reset-password?token=${token}` } else { // 降级回退:使用安全的请求头获取方式 const host = getRequestHost(event) const finalProtocol = getRequestProtocol(event) resetLink = `${finalProtocol}://${host}/reset-password?token=${token}` } const smtp = SmtpService.getInstance() // 确保SMTP配置已初始化 await smtp.initializeSmtpConfig() const htmlContent = smtp.generateEmailTemplate( '重置密码', `

您好,您请求了重置密码。

请点击下方按钮重置密码(链接在15分钟内有效)。

如果您没有请求重置密码,请忽略此邮件。

`, resetLink, formatIPForEmail(clientIP), true ) event.waitUntil( smtp .sendMail(user.email, '重置密码 | VoiceHub', htmlContent, undefined, clientIP) .catch((err) => console.error('发送重置密码邮件失败:', err)) ) } return { success: true, step: 3, message: '如果账号名和邮箱匹配,重置密码链接已发送至您的邮箱。请查收并按照邮件中的说明重置密码。' } } catch (error: any) { if (error.statusCode) { throw error } console.error('重置密码请求失败:', error) throw createApiError(500, 'AUTH_SYSTEM_ERROR', '系统错误,请稍后重试') } })