Files
BYW-VoiceHub/server/api/auth/login.post.ts
T
Henry_Du bd8df1f026
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 6m31s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
- 新增 /api/admin/songs/batch-reject.post 批量驳回接口(事务清排期/投票/扣接纳数/黑名单去重/异步通知)
- requireSongAdmin 三角色校验 + createApiError 统一错误包装
- scheduleDaysBefore/After 四字段排期可见范围限制(开放 API 与公开排期接口三处缓存路径 + DB 条件过滤)
- submissionNoteAutoModeration 基于黑名单 KEYWORD/LANGUAGE/GENRE 自动判定留言 publicStatus,命中自动关闭 submissionNotePublic
- 更新 songs/request.post 与 songs/[id]/update.put 接入自动审查逻辑
2026-09-06 17:03:20 +08:00

238 lines
6.6 KiB
TypeScript

import bcrypt from 'bcryptjs'
import { db, eq, users, userIdentities, and } from '~/drizzle/db'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
import { createAuthSession } from '~~/server/utils/create-auth-session'
import {
getAccountLockRemainingTime,
getIPBlockRemainingTime,
isAccountLocked,
isIPBlocked,
recordLoginFailure,
recordLoginSuccess,
recordAccountIpLogin,
blockUser,
getUserBlockRemainingTime
} from '../../services/securityService'
import { getBeijingTime } from '~/utils/timeUtils'
import { getClientIP } from '~~/server/utils/ip-utils'
export default defineEventHandler(async (event) => {
const startTime = Date.now()
try {
const body = await readBody(event)
const clientIp = getClientIP(event)
if (!body.username || !body.password) {
throw createError({
statusCode: 400,
message: '账号名和密码不能为空'
})
}
if (!process.env.JWT_SECRET) {
console.error('JWT_SECRET environment variable is not set')
throw createError({
statusCode: 500,
message: '服务器配置错误'
})
}
// 数据库连接检查 - 使用简单的查询测试连接
try {
await db.select().from(users).limit(1)
} catch (error) {
console.error('Database connection error:', error)
throw createError({
statusCode: 503,
message: '数据库服务暂时不可用'
})
}
// 检查IP是否被限制
if (isIPBlocked(clientIp)) {
const remainingTime = getIPBlockRemainingTime(clientIp)
throw createError({
statusCode: 423,
message: `您的IP地址已被限制访问,请在 ${remainingTime} 分钟后重试`
})
}
// 检查账户是否被锁定
if (isAccountLocked(body.username)) {
const remainingTime = getAccountLockRemainingTime(body.username)
throw createError({
statusCode: 423,
message: `账户已被锁定,请在 ${remainingTime} 分钟后重试`
})
}
// 查找用户
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,
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) {
// 记录登录失败(用户不存在)
recordLoginFailure(body.username, clientIp)
throw createError({
statusCode: 401,
message: '用户不存在'
})
}
// 验证密码
const isPasswordValid = await bcrypt.compare(body.password, user.password)
if (!isPasswordValid) {
// 记录登录失败(密码错误)
recordLoginFailure(body.username, clientIp)
throw createError({
statusCode: 401,
message: '密码不正确'
})
}
// 检查用户状态 (移到2FA之前,防止已注销用户进行2FA验证)
if (user.status === 'withdrawn') {
throw createError({
statusCode: 403,
message: '该账号已注销'
})
} else if (user.status === 'graduate') {
throw createError({
statusCode: 403,
message: '该账号已毕业,限制访问'
})
} else if (user.status === 'banned') {
throw createError({
statusCode: 403,
message: '该账号已被封禁'
})
}
// 检查是否开启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,
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
}
}
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 createError({
statusCode: 423,
message: `检测到同一账号短期多IP登录,当前IP限制 ${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, jti } = JWTEnhanced.generateTokenWithJti(user.id, user.role)
await createAuthSession(event, { userId: user.id, jti, loginMethod: '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`)
return {
success: true,
user: {
id: user.id,
username: user.username,
name: user.name,
grade: user.grade,
class: user.class,
studentId: user.studentId,
role: user.role,
needsPasswordChange: !user.passwordChangedAt
}
}
} catch (error: any) {
const errorTime = Date.now() - startTime
console.error(`Login error after ${errorTime}ms:`, error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
message: '登录过程中发生未知错误'
})
}
})