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 接入自动审查逻辑
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
|
||||
interface RoutePolicy {
|
||||
path: string
|
||||
methods: readonly HttpMethod[]
|
||||
}
|
||||
|
||||
const GET_METHODS = ['GET', 'HEAD'] as const
|
||||
const WRITE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'] as const
|
||||
|
||||
export const PASSWORD_CHANGE_ALLOWED_ROUTES: readonly RoutePolicy[] = [
|
||||
{ path: '/api/auth/verify', methods: GET_METHODS },
|
||||
{ path: '/api/auth/logout', methods: ['POST'] },
|
||||
{ path: '/api/auth/change-password', methods: ['POST'] },
|
||||
{ path: '/api/auth/set-initial-password', methods: ['POST'] },
|
||||
{ path: '/api/auth/2fa/verify', methods: ['POST'] },
|
||||
{ path: '/api/auth/2fa/send-email', methods: ['POST'] },
|
||||
{ path: '/api/site-config', methods: GET_METHODS }
|
||||
]
|
||||
|
||||
export const PUBLIC_API_EXACT_ROUTES: readonly RoutePolicy[] = [
|
||||
{ path: '/api/auth/login', methods: ['POST'] },
|
||||
{ path: '/api/auth/register', methods: ['POST'] },
|
||||
{ path: '/api/auth/email-code', methods: ['POST'] },
|
||||
{ path: '/api/auth/grade-class-options', methods: GET_METHODS },
|
||||
{ path: '/api/auth/oauth-register-options', methods: GET_METHODS },
|
||||
{ path: '/api/auth/bind', methods: ['POST'] },
|
||||
{ path: '/api/auth/oauth-register', methods: ['POST'] },
|
||||
{ path: '/api/auth/2fa/verify', methods: ['POST'] },
|
||||
{ path: '/api/auth/2fa/send-email', methods: ['POST'] },
|
||||
{ path: '/api/auth/forgot-password', methods: ['POST'] },
|
||||
{ path: '/api/auth/reset-password', methods: ['POST'] },
|
||||
{ path: '/api/auth/captcha', methods: GET_METHODS },
|
||||
{ path: '/api/semesters/current', methods: GET_METHODS },
|
||||
{ path: '/api/play-times', methods: GET_METHODS },
|
||||
{ path: '/api/schedules/public', methods: GET_METHODS },
|
||||
{ path: '/api/songs/count', methods: GET_METHODS },
|
||||
{ path: '/api/songs/public', methods: GET_METHODS },
|
||||
{ path: '/api/site-config', methods: GET_METHODS },
|
||||
{ path: '/api/system/location', methods: GET_METHODS },
|
||||
{ path: '/api/auth/webauthn/login', methods: ['POST'] },
|
||||
{ path: '/api/music/resolve-url', methods: ['POST'] },
|
||||
{ path: '/api/music/state', methods: ['POST'] },
|
||||
{ path: '/api/music/websocket', methods: GET_METHODS },
|
||||
{ path: '/api/sys/time', methods: GET_METHODS }
|
||||
]
|
||||
|
||||
export const PUBLIC_API_PREFIX_ROUTES: readonly RoutePolicy[] = [
|
||||
{ path: '/api/proxy/', methods: GET_METHODS },
|
||||
{ path: '/api/api-enhanced/', methods: [...GET_METHODS, ...WRITE_METHODS] },
|
||||
{ path: '/api/native-api/', methods: [...GET_METHODS, 'POST'] },
|
||||
{ path: '/api/open/', methods: [...GET_METHODS, ...WRITE_METHODS] },
|
||||
{ path: '/api/auth/webauthn/login/', methods: ['POST'] }
|
||||
]
|
||||
|
||||
function matchesMethod(policy: RoutePolicy, method: string): boolean {
|
||||
return policy.methods.includes(method.toUpperCase() as HttpMethod)
|
||||
}
|
||||
|
||||
export function isPublicApiPath(pathname: string, method: string): boolean {
|
||||
return (
|
||||
PUBLIC_API_EXACT_ROUTES.some(
|
||||
(policy) => pathname === policy.path && matchesMethod(policy, method)
|
||||
) ||
|
||||
PUBLIC_API_PREFIX_ROUTES.some(
|
||||
(policy) => pathname.startsWith(policy.path) && matchesMethod(policy, method)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function isAllowedDuringPasswordChange(pathname: string, method: string): boolean {
|
||||
return PASSWORD_CHANGE_ALLOWED_ROUTES.some(
|
||||
(policy) => pathname === policy.path && matchesMethod(policy, method)
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldBlockDuringPasswordChange(
|
||||
pathname: string,
|
||||
method: string,
|
||||
requirePasswordChange: boolean
|
||||
): boolean {
|
||||
return (
|
||||
requirePasswordChange &&
|
||||
!isPublicApiPath(pathname, method) &&
|
||||
!isAllowedDuringPasswordChange(pathname, method)
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldBypassPublicApiAuthentication(
|
||||
pathname: string,
|
||||
method: string,
|
||||
hasToken: boolean
|
||||
): boolean {
|
||||
return isPublicApiPath(pathname, method) && !hasToken
|
||||
}
|
||||
|
||||
export function canBindOAuthIdentity(requirePasswordChange: boolean): boolean {
|
||||
return !requirePasswordChange
|
||||
}
|
||||
Reference in New Issue
Block a user