- 新增 /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 接入自动审查逻辑
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
import { getHeader } from 'h3'
|
|
import { db, eq, and } from '~/drizzle/db'
|
|
import { authSessions } from '~/drizzle/schema'
|
|
import { parseUserAgent, type LoginMethod } from './user-agent-info'
|
|
import { getClientIP } from './ip-utils'
|
|
import { randomUUID } from 'node:crypto'
|
|
|
|
export interface CreateAuthSessionOptions {
|
|
userId: number
|
|
jti: string
|
|
loginMethod: LoginMethod
|
|
expiresAt?: Date
|
|
oauthProvider?: string
|
|
}
|
|
|
|
const DEFAULT_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000
|
|
|
|
export async function createAuthSession(
|
|
event: any,
|
|
options: CreateAuthSessionOptions
|
|
): Promise<string> {
|
|
const { userId, jti, loginMethod, oauthProvider } = options
|
|
const ua = getHeader(event, 'user-agent')
|
|
const { browser, operatingSystem, device } = parseUserAgent(ua)
|
|
const ip = getClientIP(event)
|
|
const now = new Date()
|
|
const expiresAt = options.expiresAt ?? new Date(now.getTime() + DEFAULT_SESSION_TTL_MS)
|
|
|
|
try {
|
|
let method = loginMethod
|
|
if (oauthProvider && method === 'oauth') {
|
|
method = `oauth:${oauthProvider}` as any
|
|
}
|
|
|
|
await db.insert(authSessions).values({
|
|
id: randomUUID(),
|
|
userId,
|
|
sessionToken: jti,
|
|
browser,
|
|
operatingSystem,
|
|
device,
|
|
loginMethod: method,
|
|
ipAddress: ip,
|
|
userAgent: ua ?? null,
|
|
createdAt: now,
|
|
lastActiveAt: now,
|
|
expiresAt,
|
|
isRevoked: false
|
|
})
|
|
} catch (error) {
|
|
console.warn('[AuthSession] 创建会话失败,继续无会话登录:', error)
|
|
}
|
|
|
|
return jti
|
|
}
|
|
|
|
export async function isSessionRevokedByJti(jti: string | undefined): Promise<boolean> {
|
|
if (!jti) return false
|
|
try {
|
|
const row = await db.query.authSessions.findFirst({
|
|
where: and(eq(authSessions.sessionToken, jti)),
|
|
columns: { isRevoked: true, expiresAt: true }
|
|
})
|
|
if (!row) return false
|
|
if (row.expiresAt && row.expiresAt.getTime() < Date.now()) return true
|
|
return Boolean(row.isRevoked)
|
|
} catch (error) {
|
|
console.warn('[AuthSession] 会话状态校验失败,默认放行:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function touchAuthSession(jti: string | undefined): Promise<void> {
|
|
if (!jti) return
|
|
try {
|
|
await db
|
|
.update(authSessions)
|
|
.set({ lastActiveAt: new Date() })
|
|
.where(eq(authSessions.sessionToken, jti))
|
|
} catch (_error) {
|
|
// ignore
|
|
}
|
|
}
|