Files
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

184 lines
5.7 KiB
TypeScript

import { parseState, getRedirectUri } from '~~/server/utils/oauth'
import { generateBindingToken } from '~~/server/utils/oauth-token'
import { db, eq, userIdentities } from '~/drizzle/db'
import { JWTEnhanced } from '~~/server/utils/jwt-enhanced'
import { createAuthSession } from '~~/server/utils/create-auth-session'
import { getOAuthStrategy } from '~~/server/utils/oauth-strategies'
import { isUserBlocked, getUserBlockRemainingTime } from '~~/server/services/securityService'
export default defineEventHandler(async (event) => {
const provider = getRouterParam(event, 'provider')
const query = getQuery(event)
const code = query.code as string
const stateStr = query.state as string
if (!provider) {
throw createError({ statusCode: 400, message: 'Missing provider' })
}
if (!code || !stateStr) {
throw createError({ statusCode: 400, message: 'Missing code or state' })
}
// 1. 验证 State
const csrfCookie = getCookie(event, 'oauth_csrf')
// 获取 Origin
const headers = getRequestHeaders(event)
const protocol = headers['x-forwarded-proto'] || 'http'
const host = headers['host']
const origin = `${protocol}://${host}`
const state = parseState(stateStr, origin, csrfCookie)
if (!state) {
throw createError({ statusCode: 400, message: 'Invalid or expired state' })
}
// 清除 CSRF cookie
deleteCookie(event, 'oauth_csrf')
const strategy = getOAuthStrategy(provider)
const redirectUri = getRedirectUri(provider)
// 2. 使用 Code 换取 Token
let accessToken = ''
try {
accessToken = await strategy.exchangeToken(code, redirectUri)
} catch (e: any) {
console.error(`[OAuth] ${provider} token exchange failed:`, e.message)
return sendRedirect(
event,
`/auth/error?code=TOKEN_EXCHANGE_FAILED&message=${encodeURIComponent('授权失败,无法获取访问令牌')}`
)
}
// 3. 获取用户信息
let userInfo
try {
userInfo = await strategy.getUserInfo(accessToken)
} catch (e: any) {
console.error(`[OAuth] ${provider} get user info failed:`, e.message)
return sendRedirect(
event,
`/auth/error?code=USER_INFO_FAILED&message=${encodeURIComponent('获取用户信息失败')}`
)
}
const providerUserId = userInfo.id
const providerUsername = userInfo.username
return handleUserLoginOrBind(event, provider, providerUserId, providerUsername)
})
async function handleUserLoginOrBind(
event: any,
provider: string,
providerUserId: string,
providerUsername: string
) {
// 4. 检查是否已登录(绑定模式)
const authToken = getCookie(event, 'auth-token')
let currentUser: any = null
if (authToken) {
try {
const payload = JWTEnhanced.verifyToken(authToken)
currentUser = payload
} catch (e) {
// Token 无效或已过期,忽略
}
}
// 5. 检查身份关联
const existingIdentity = await db.query.userIdentities.findFirst({
where: (t, { eq, and }) => and(eq(t.provider, provider), eq(t.providerUserId, providerUserId)),
with: { user: true }
})
// 如果用户已登录,则是绑定操作
if (currentUser) {
if (existingIdentity) {
// 已经被绑定
if (existingIdentity.userId === currentUser.userId) {
// 已经被当前用户绑定
return sendRedirect(event, '/account?message=' + encodeURIComponent('账号已绑定'))
} else {
// 已经被其他用户绑定
return sendRedirect(event, '/account?error=' + encodeURIComponent('该账号已被其他用户绑定'))
}
} else {
// 未被绑定,直接绑定到当前用户
await db.insert(userIdentities).values({
userId: currentUser.userId,
provider: provider,
providerUserId: providerUserId,
providerUsername: providerUsername,
createdAt: new Date()
})
return sendRedirect(event, '/account?message=' + encodeURIComponent('绑定成功'))
}
}
// 未登录,则是登录或新绑定流程
if (existingIdentity && existingIdentity.user) {
// 检查用户状态
const user = existingIdentity.user
if (user.status === 'withdrawn') {
return sendRedirect(
event,
`/auth/error?code=ACCOUNT_WITHDRAWN&message=${encodeURIComponent('账号已注销')}`
)
}
if (user.status === 'graduate') {
return sendRedirect(
event,
`/auth/error?code=ACCOUNT_GRADUATED&message=${encodeURIComponent('该账号已毕业,限制访问')}`
)
}
if (isUserBlocked(user.id)) {
const remaining = getUserBlockRemainingTime(user.id)
return sendRedirect(
event,
`/auth/error?code=ACCOUNT_BLOCKED&message=${encodeURIComponent(`账户处于风险控制期,请在 ${remaining} 分钟后重试`)}`
)
}
// 登录
const { token, jti } = JWTEnhanced.generateTokenWithJti(existingIdentity.user.id, existingIdentity.user.role)
await createAuthSession(event, {
userId: existingIdentity.user.id,
jti,
loginMethod: 'oauth',
oauthProvider: provider
})
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
path: '/'
})
return sendRedirect(event, '/')
} else {
// 绑定
const bindingToken = generateBindingToken({
provider: provider,
providerUserId,
providerUsername
})
// 将绑定令牌存入 cookie
setCookie(event, 'binding-token', bindingToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 10, // 10分钟
path: '/'
})
return sendRedirect(
event,
`/login?action=bind&provider=${provider}&username=${encodeURIComponent(providerUsername)}`
)
}
}