初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
@@ -0,0 +1,102 @@
import { db, users, eq } from '~/drizzle/db'
import { twoFactorCodes } from '~~/server/utils/twoFactorStore'
import { SmtpService } from '~~/server/services/smtpService'
import { getClientIP } from '~~/server/utils/ip-utils'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
import { randomInt } from 'crypto'
export default defineEventHandler(async (event) => {
const { userId: reqUserId, token, email } = await readBody(event)
// 必须提供预认证令牌
if (!token) {
throw createError({ statusCode: 400, message: '缺少预认证令牌' })
}
let userId: number
try {
const decoded = JWTEnhanced.verify(token) as any
if (decoded.type !== 'pre-auth' || decoded.scope !== '2fa_pending') {
throw new Error('无效的预认证令牌')
}
userId = decoded.userId
} catch (e) {
throw createError({ statusCode: 401, message: '会话已失效,请重新登录' })
}
// 校验 userId 是否匹配
if (!reqUserId || Number(reqUserId) !== userId) {
throw createError({ statusCode: 403, message: '非法操作:用户ID不匹配' })
}
// 获取用户邮箱
const userResult = await db.select({
id: users.id,
email: users.email,
emailVerified: users.emailVerified,
name: users.name
})
.from(users)
.where(eq(users.id, userId))
.limit(1)
const user = userResult[0]
// 增加 emailVerified 校验
if (!user || !user.email || !user.emailVerified) {
throw createError({ statusCode: 400, message: '用户不存在或未绑定邮箱' })
}
// 校验用户输入的邮箱是否匹配
if (!email || email.trim().toLowerCase() !== user.email.toLowerCase()) {
throw createError({ statusCode: 400, message: '邮箱地址不匹配' })
}
// 检查是否在冷却时间内
const existingCode = twoFactorCodes.get(userId)
if (existingCode && existingCode.expiresAt > Date.now()) {
// 5 * 60 * 1000 = 300000ms
const totalDuration = 5 * 60 * 1000
const timePassed = totalDuration - (existingCode.expiresAt - Date.now())
if (timePassed < 60 * 1000) { // 60秒冷却
const remainingSeconds = Math.ceil((60000 - timePassed) / 1000)
throw createError({
statusCode: 429,
message: `操作过于频繁,请等待 ${remainingSeconds} 秒后再试`
})
}
}
const code = randomInt(100000, 999999).toString()
twoFactorCodes.set(userId, {
code,
expiresAt: Date.now() + 5 * 60 * 1000,
attempts: 0
})
const clientIP = getClientIP(event)
// 发送邮件
const smtp = SmtpService.getInstance()
await smtp.initializeSmtpConfig()
const sent = await smtp.renderAndSend(
user.email,
'verification.code',
{
name: user.name || '用户',
email: user.email,
code,
expiresInMinutes: 5,
action: '登录验证'
},
clientIP
)
if (!sent) {
throw createError({ statusCode: 500, message: '验证码发送失败' })
}
return { success: true, message: '验证码已发送至您的邮箱' }
})
@@ -0,0 +1,130 @@
import otplib from 'otplib'
const { authenticator } = otplib
import { db, userIdentities, eq, and, users } from '~/drizzle/db'
import { twoFactorCodes } from '~~/server/utils/twoFactorStore'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
import { getClientIP } from '~~/server/utils/ip-utils'
import { getBeijingTime } from '~/utils/timeUtils'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { code, type, token } = body
if (!code || !type) {
throw createError({ statusCode: 400, message: '缺少必要参数' })
}
// 验证预认证临时令牌
let targetUserId: number
if (token) {
try {
const decoded = JWTEnhanced.verify(token) as any
if (decoded.type !== 'pre-auth' || decoded.scope !== '2fa_pending') {
throw new Error('无效的预认证令牌')
}
targetUserId = decoded.userId
} catch (e) {
throw createError({ statusCode: 401, message: '会话已失效,请重新登录' })
}
} else {
// 强制要求 Token
throw createError({ statusCode: 400, message: '缺少预认证令牌,请重新登录' })
}
// 获取用户信息
const userResult = await db.select().from(users).where(eq(users.id, targetUserId)).limit(1)
const user = userResult[0]
if (!user) {
throw createError({ statusCode: 404, message: '用户不存在' })
}
if (user.status !== 'active') {
throw createError({ statusCode: 403, message: '账号已被禁用或限制访问' })
}
let verified = false
if (type === 'totp') {
const identity = await db.query.userIdentities.findFirst({
where: and(eq(userIdentities.userId, targetUserId), eq(userIdentities.provider, 'totp'))
})
if (!identity) {
throw createError({ statusCode: 400, message: '未开启TOTP验证' })
}
verified = authenticator.check(code, identity.providerUserId)
if (!verified) {
throw createError({ statusCode: 400, message: '动态验证码错误' })
}
} else if (type === 'email') {
const stored = twoFactorCodes.get(targetUserId)
if (!stored) {
throw createError({ statusCode: 400, message: '验证码已过期或不存在' })
}
if (stored.expiresAt <= Date.now()) {
twoFactorCodes.delete(targetUserId)
throw createError({ statusCode: 400, message: '验证码已过期' })
}
// 检查尝试次数
if (stored.attempts >= 5) {
twoFactorCodes.delete(targetUserId)
throw createError({ statusCode: 400, message: '验证尝试次数过多,请重新获取' })
}
if (stored.code === code) {
verified = true
twoFactorCodes.delete(targetUserId) // 验证成功后删除
} else {
// 增加尝试次数
stored.attempts++
twoFactorCodes.set(targetUserId, stored)
throw createError({ statusCode: 400, message: `验证码错误,剩余尝试次数:${5 - stored.attempts}` })
}
} else {
throw createError({ statusCode: 400, message: '不支持的验证类型' })
}
// 验证通过,更新登录信息
const clientIp = getClientIP(event)
await db.update(users)
.set({
lastLogin: getBeijingTime(),
lastLoginIp: clientIp
})
.where(eq(users.id, user.id))
.catch((err) => console.error('Error updating user login info:', err))
// 生成Token
const authToken = JWTEnhanced.generateToken(user.id, user.role)
const isSecure =
getRequestURL(event).protocol === 'https:' ||
getRequestHeader(event, 'x-forwarded-proto') === 'https'
setCookie(event, 'auth-token', authToken, {
httpOnly: true,
secure: isSecure,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
path: '/'
})
return {
success: true,
user: {
id: user.id,
username: user.username,
name: user.name,
grade: user.grade,
class: user.class,
role: user.role,
needsPasswordChange: !user.passwordChangedAt,
has2FA: true
}
}
})