import bcrypt from 'bcryptjs' import { db, eq, users, userIdentities, and, systemSettings } from '~/drizzle/db' import { JWTEnhanced } from '~~/server/utils/jwt-enhanced' import { getAccountLockRemainingTime, getIPBlockRemainingTime, isAccountLocked, isIPBlocked, recordLoginFailure, recordLoginSuccess, recordAccountIpLogin, blockUser, getUserBlockRemainingTime, //导入失败计数查询函数 getLoginFailureCount } from '../../services/securityService' import { getBeijingTime } from '~/utils/timeUtils' import { getClientIP } from '~~/server/utils/ip-utils' import { resolveRequirePasswordChange } from '~~/server/utils/system-settings-helper' import { getPasswordSetupState } from '~~/server/utils/initial-password-policy' // 导入验证码校验函数 import { verifyAndConsumeCaptcha } from '~~/server/utils/captcha' import type { SystemSettings } from '~/drizzle/schema' import { createApiError } from '~~/server/utils/apiError' import { createAuthSession } from '~~/server/utils/auth-session' export default defineEventHandler(async (event) => { const startTime = Date.now() try { const body = await readBody(event) const clientIp = getClientIP(event) let captchaId = '' let captchaInput = '' if (!body.username || !body.password) { throw createApiError(400, 'AUTH_USERNAME_PASSWORD_REQUIRED', '用户名和密码不能为空') } if (!process.env.JWT_SECRET) { console.error('JWT_SECRET environment variable is not set') throw createApiError(500, 'AUTH_SERVER_CONFIG_ERROR', '服务器配置错误') } // 数据库连接检查 - 使用简单的查询测试连接 try { await db.select().from(users).limit(1) } catch (error) { console.error('Database connection error:', error) throw createApiError(503, 'AUTH_DATABASE_UNAVAILABLE', '数据库服务暂时不可用') } // 检查IP是否被限制 if (isIPBlocked(clientIp)) { const remainingTime = getIPBlockRemainingTime(clientIp) throw createApiError(423, 'AUTH_IP_BLOCKED_RETRY_MINUTES', `您的IP地址已被限制访问,请在 ${remainingTime} 分钟后重试`, { params: [remainingTime] }) } // 检查账户是否被锁定 if (await isAccountLocked(body.username)) { const remainingTime = await getAccountLockRemainingTime(body.username) throw createApiError(423, 'AUTH_ACCOUNT_LOCKED_MINUTES', `账户已被锁定,请在 ${remainingTime} 分钟后重试`, { params: [remainingTime] }) } // 读取全局配置:是否启用图形验证码 let captchaEnabled = false let captchaProvider = 'graphic' let turnstileSecretKey = '' let captchaMaxFailures = 3 try { const settings = await db .select() .from(systemSettings) .limit(1) .then((rows) => rows[0] as SystemSettings | undefined) if (settings?.captchaEnabled) { captchaEnabled = true captchaProvider = settings.captchaProvider || 'graphic' turnstileSecretKey = settings.turnstileSecretKey || '' if (settings.captchaMaxFailures) { captchaMaxFailures = settings.captchaMaxFailures } } } catch (e) { // 查询异常(如表不存在)时默认关闭验证码,保证登录可用 console.warn('读取验证码配置失败,已暂时禁用:', e) } // 图形验证码检查 let needCaptcha = false if (captchaEnabled) { if (captchaProvider === 'turnstile') { needCaptcha = true // Turnstile 每次都验证 } else { const failCount = await getLoginFailureCount(body.username) needCaptcha = failCount >= captchaMaxFailures } } // 验证码校验 if (needCaptcha) { if (captchaProvider === 'turnstile') { const turnstileToken = body.turnstileToken if (!turnstileSecretKey) { console.error('Turnstile is enabled but secret key is missing!') throw createApiError(500, 'AUTH_CODE_SERVICE_CONFIG_ERROR', '验证码服务配置错误,请联系管理员') } if (!turnstileToken) { throw createApiError(400, 'AUTH_CAPTCHA_REQUIRED', '请完成人机验证', { captchaRequired: true, captchaProvider: 'turnstile' }) } const verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify' const formData = new URLSearchParams() formData.append('secret', turnstileSecretKey) formData.append('response', turnstileToken) formData.append('remoteip', clientIp) try { const result: any = await $fetch(verifyUrl, { method: 'POST', body: formData, timeout: 5000 }) if (!result.success) { console.error('Turnstile verification failed:', result['error-codes']) throw createApiError(400, 'AUTH_CAPTCHA_FAILED_OR_EXPIRED', '人机验证失败或已过期,请重试', { captchaRequired: true, captchaProvider: 'turnstile' }) } } catch (err: any) { if (err.statusCode === 400) throw err console.error('Turnstile verification error:', err) throw createApiError(500, 'AUTH_CAPTCHA_SERVICE_UNAVAILABLE', '人机验证服务暂时不可用') } } else { captchaId = body.captchaId captchaInput = body.captchaInput if (!captchaId || !captchaInput) { throw createApiError(400, 'AUTH_IMAGE_CAPTCHA_REQUIRED', '请完成图形验证码', { captchaRequired: true, captchaProvider: 'graphic' }) } const isValid = await verifyAndConsumeCaptcha(captchaId, captchaInput) if (!isValid) { throw createApiError(400, 'AUTH_CODE_INVALID_OR_EXPIRED', '验证码错误或已过期,请重新输入', { captchaRequired: true, captchaProvider: 'graphic' }) } } } // 查找用户 const userResult = await db .select({ id: users.id, username: users.username, name: users.name, grade: users.grade, class: users.class, password: users.password, role: users.role, lastLogin: users.lastLogin, lastLoginIp: users.lastLoginIp, passwordChangedAt: users.passwordChangedAt, forcePasswordChange: users.forcePasswordChange, tokenVersion: users.tokenVersion, status: users.status, email: users.email, emailVerified: users.emailVerified }) .from(users) .where(eq(users.username, body.username)) .limit(1) const user = userResult[0] || null if (!user) { // 记录登录失败(用户不存在) await recordLoginFailure(body.username, clientIp) throw createApiError(401, 'AUTH_INVALID_CREDENTIALS', '用户名或密码错误') } // 验证密码 const isPasswordValid = await bcrypt.compare(body.password, user.password) if (!isPasswordValid) { // 记录登录失败(密码错误) await recordLoginFailure(body.username, clientIp) throw createApiError(401, 'AUTH_INVALID_CREDENTIALS', '用户名或密码错误') } // 检查用户状态 (移到2FA之前,防止已退学用户进行2FA验证) if (user.status === 'withdrawn') { throw createApiError(403, 'AUTH_ACCOUNT_WITHDRAWN', '该账号已退学,限制访问') } else if (user.status === 'graduate') { throw createApiError(403, 'AUTH_ACCOUNT_GRADUATED', '该账号已毕业,限制访问') } else if (user.status === 'banned') { throw createApiError(403, 'AUTH_ACCOUNT_BANNED', '该账号已被封禁') } else if (user.status === 'pending') { throw createApiError(403, 'AUTH_USER_PENDING_APPROVAL', '账号待管理员审核,请耐心等待') } else if (user.status === 'rejected') { throw createApiError(403, 'AUTH_ACCOUNT_CURRENTLY_UNAVAILABLE', '该账号当前不可用') } // 普通登录不延续绑定流程,避免残留绑定令牌被后续 2FA 校验意外消费 deleteCookie(event, 'binding-token') // 检查是否开启2FA const totpIdentity = await db.query.userIdentities.findFirst({ where: and(eq(userIdentities.userId, user.id), eq(userIdentities.provider, 'totp')) }) if (totpIdentity) { // 生成预认证临时令牌 const tempToken = JWTEnhanced.sign( { userId: user.id, tokenVersion: user.tokenVersion, type: 'pre-auth', scope: '2fa_pending' }, { expiresIn: '5m' } ) // 动态构建验证方式列表 const methods = ['totp'] let maskedEmail = '' if (user.email && user.emailVerified) { methods.push('email') // 生成脱敏邮箱提示 const [local, domain] = user.email.split('@') if (local && domain) { maskedEmail = local.length <= 2 ? `***@${domain}` : `${local.slice(0, 2)}****@${domain}` } } return { success: true, requires2FA: true, userId: user.id, methods, maskedEmail, tempToken } } await recordLoginSuccess(body.username, clientIp) const ipSwitchExceeded = recordAccountIpLogin(body.username, clientIp) if (ipSwitchExceeded) { blockUser(user.id) const ipRemain = getIPBlockRemainingTime(clientIp) const userRemain = getUserBlockRemainingTime(user.id) throw createApiError(423, 'AUTH_MULTI_IP_PROTECTION', `检测到同一账号短期多IP登录,当前IP限制 ${ipRemain} 分钟,账户保护 ${userRemain} 分钟`, { params: [ipRemain, userRemain] }) } // 更新登录信息 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)) // 生成JWT const { token } = await createAuthSession(event, user, 'password') // 自动判断是否需要secure const isSecure = getRequestURL(event).protocol === 'https:' || getRequestHeader(event, 'x-forwarded-proto') === 'https' // 设置cookie setCookie(event, 'auth-token', token, { httpOnly: true, secure: isSecure, sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, // 7天 path: '/' }) const processingTime = Date.now() - startTime console.log(`Login for ${user.username} processed in ${processingTime}ms`) const requirePasswordChange = await resolveRequirePasswordChange(user) const passwordSetupState = getPasswordSetupState(user, requirePasswordChange) return { success: true, user: { id: user.id, username: user.username, name: user.name, grade: user.grade, class: user.class, role: user.role, email: user.email, emailVerified: user.emailVerified, forcePasswordChange: user.forcePasswordChange, passwordChangedAt: user.passwordChangedAt, requirePasswordChange, // 兼容旧客户端字段名 needsPasswordChange: requirePasswordChange, ...passwordSetupState } } } catch (error: any) { const errorTime = Date.now() - startTime console.error(`Login error after ${errorTime}ms:`, error) if (error.statusCode) { throw error } throw createApiError(500, 'AUTH_LOGIN_UNKNOWN_ERROR', '登录过程中发生未知错误') } })