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

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)
}