Files
BYW-VoiceHub/server/api/auth/bind.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

110 lines
3.3 KiB
TypeScript

import bcrypt from 'bcryptjs'
import { db, eq, users, userIdentities } from '~/drizzle/db'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
import { createAuthSession } from '~~/server/utils/create-auth-session'
import { verifyBindingToken } from '~~/server/utils/oauth-token'
import {
isAccountLocked,
recordLoginFailure,
recordLoginSuccess
} from '~~/server/services/securityService'
import { getClientIP } from '~~/server/utils/ip-utils'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { username, password } = body
const bindingToken = getCookie(event, 'binding-token')
if (!bindingToken) {
throw createError({ statusCode: 400, message: '绑定会话已过期,请重新通过第三方登录发起' })
}
let payload
try {
payload = verifyBindingToken(bindingToken)
} catch (e) {
deleteCookie(event, 'binding-token')
throw createError({ statusCode: 400, message: '无效的绑定令牌' })
}
const clientIp = getClientIP(event)
// 验证用户凭据
if (isAccountLocked(username)) {
throw createError({ statusCode: 423, message: '账户已被锁定,请稍后重试' })
}
const user = await db.query.users.findFirst({
where: eq(users.username, username)
})
if (!user) {
recordLoginFailure(username, clientIp)
throw createError({ statusCode: 401, message: '用户名或密码错误' })
}
const valid = await bcrypt.compare(password, user.password)
if (!valid) {
recordLoginFailure(username, clientIp)
throw createError({ statusCode: 401, message: '用户名或密码错误' })
}
// 绑定
try {
await db.transaction(async (tx) => {
const existing = await tx.query.userIdentities.findFirst({
where: (t, { eq, and }) =>
and(eq(t.provider, payload.provider), eq(t.providerUserId, payload.providerUserId))
})
if (existing) {
// 已经被绑定,不需要再次插入
return
}
await tx.insert(userIdentities).values({
userId: user.id,
provider: payload.provider,
providerUserId: payload.providerUserId,
providerUsername: payload.providerUsername
})
})
} catch (e: any) {
// 如果用户尝试再次绑定相同的账户,处理唯一约束违规
if (e.code === '23505') {
// Postgres 唯一性冲突
// 这意味着身份已经绑定,这是正常情况。
// 我们可以继续执行登录流程。
} else {
// 绑定失败,清除 cookie 防止重放,或者保留允许重试?
// 如果是系统错误,保留 cookie 可能更好。
// 但如果是逻辑错误,应该清除。
// 这里我们选择抛出错误,前端处理。
throw e
}
}
recordLoginSuccess(username, clientIp)
// 登录
const { token, jti } = JWTEnhanced.generateTokenWithJti(user.id, user.role)
await createAuthSession(event, {
userId: user.id,
jti,
loginMethod: 'oauth',
oauthProvider: payload.provider
})
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
path: '/'
})
// 清除绑定令牌
deleteCookie(event, 'binding-token')
return { success: true }
})