- 新增 /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 接入自动审查逻辑
127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
export type OAuthProvider = 'github' | 'casdoor' | 'google' | 'oauth2' | 'aggregate'
|
|
|
|
export const AGGREGATE_OAUTH_LOGIN_TYPES = [
|
|
'qq',
|
|
'wx',
|
|
'alipay',
|
|
'sina',
|
|
'baidu',
|
|
'douyin',
|
|
'huawei',
|
|
'xiaomi',
|
|
'gitee',
|
|
'gitea',
|
|
'bilibili',
|
|
'kuaishou'
|
|
] as const
|
|
|
|
export type AggregateOAuthLoginType = (typeof AGGREGATE_OAUTH_LOGIN_TYPES)[number]
|
|
|
|
const isPrivateIPv4 = (hostname: string): boolean => {
|
|
const parts = hostname.split('.').map(Number)
|
|
if (
|
|
parts.length !== 4 ||
|
|
parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
|
|
) {
|
|
return false
|
|
}
|
|
|
|
const [first, second] = parts
|
|
return (
|
|
first === 10 ||
|
|
first === 127 ||
|
|
(first === 100 && second >= 64 && second <= 127) ||
|
|
(first === 169 && second === 254) ||
|
|
(first === 172 && second >= 16 && second <= 31) ||
|
|
(first === 192 && second === 168)
|
|
)
|
|
}
|
|
|
|
const isPrivateIPv6 = (hostname: string): boolean => {
|
|
const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase()
|
|
if (!normalized.includes(':')) return false
|
|
if (normalized.startsWith('::ffff:')) {
|
|
return isPrivateIPv4(normalized.slice('::ffff:'.length))
|
|
}
|
|
return (
|
|
normalized === '::1' ||
|
|
normalized.startsWith('fc') ||
|
|
normalized.startsWith('fd') ||
|
|
/^fe[89ab]/.test(normalized)
|
|
)
|
|
}
|
|
|
|
const isInternalHostname = (hostname: string): boolean => {
|
|
const normalized = hostname.toLowerCase()
|
|
if (normalized.includes(':')) return isPrivateIPv6(normalized)
|
|
if (isPrivateIPv4(normalized)) return true
|
|
return (
|
|
normalized === 'localhost' ||
|
|
!normalized.includes('.') ||
|
|
normalized.endsWith('.localhost') ||
|
|
normalized.endsWith('.local') ||
|
|
normalized.endsWith('.lan') ||
|
|
normalized.endsWith('.internal') ||
|
|
normalized.endsWith('.home.arpa')
|
|
)
|
|
}
|
|
|
|
export const isSafeAggregateOAuthUrl = (value: string): boolean => {
|
|
try {
|
|
const url = new URL(value)
|
|
if (url.username || url.password) return false
|
|
if (url.protocol === 'https:') return true
|
|
if (url.protocol !== 'http:') return false
|
|
return isInternalHostname(url.hostname)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export const normalizeAggregateOAuthLoginTypes = (value: unknown): AggregateOAuthLoginType[] => {
|
|
let values: unknown[] = []
|
|
|
|
if (Array.isArray(value)) {
|
|
values = value
|
|
} else if (typeof value === 'string' && value.trim()) {
|
|
const normalized = value.trim()
|
|
try {
|
|
const parsed = JSON.parse(normalized)
|
|
values = Array.isArray(parsed) ? parsed : [parsed]
|
|
} catch {
|
|
values = normalized.split(',')
|
|
}
|
|
}
|
|
|
|
return [
|
|
...new Set(
|
|
values
|
|
.filter((item): item is string => typeof item === 'string')
|
|
.map((item) => item.trim().toLowerCase())
|
|
.filter((item): item is AggregateOAuthLoginType =>
|
|
AGGREGATE_OAUTH_LOGIN_TYPES.includes(item as AggregateOAuthLoginType)
|
|
)
|
|
)
|
|
]
|
|
}
|
|
|
|
export const getAggregateOAuthLoginTypesOrDefault = (value: unknown): AggregateOAuthLoginType[] => {
|
|
const normalized = normalizeAggregateOAuthLoginTypes(value)
|
|
const hasConfiguredValue =
|
|
(Array.isArray(value) && value.length > 0) ||
|
|
(typeof value === 'string' && value.trim().length > 0)
|
|
return normalized.length > 0 || hasConfiguredValue ? normalized : ['qq']
|
|
}
|
|
|
|
export const SUPPORTED_OAUTH_PROVIDERS: OAuthProvider[] = [
|
|
'github',
|
|
'casdoor',
|
|
'google',
|
|
'oauth2',
|
|
'aggregate'
|
|
]
|
|
|
|
export const isSupportedOAuthProvider = (provider: string): provider is OAuthProvider => {
|
|
return SUPPORTED_OAUTH_PROVIDERS.includes(provider as OAuthProvider)
|
|
}
|