- 新增 /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 接入自动审查逻辑
184 lines
5.7 KiB
TypeScript
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)}`
|
|
)
|
|
}
|
|
}
|