feat(api): enhance IP list management with new entry operations and improved error handling
- Added endpoints for adding and deleting entries in IP lists. - Refactored list creation logic to handle manual list types more effectively. - Updated refresh logic to rebuild manual list entries. - Improved error handling for entry operations to ensure data integrity. - Enhanced response structure for list detail retrieval. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -85,6 +85,7 @@ export const ipOverrideSchema = z.object({
|
||||
|
||||
export const createIpListBodySchema = z.object({
|
||||
name: z.string().min(1),
|
||||
/** Prefer static | json_url | evobgp_community; domains accepted for legacy. */
|
||||
type: ipListTypeSchema,
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
entries: z.array(z.string()).optional(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './contracts.js'
|
||||
export * from './list-entries.js'
|
||||
export * from './permissions.js'
|
||||
export * from './app-switcher.js'
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/** DB type codes — never show raw in UI. */
|
||||
export const IP_LIST_SOURCE_LABELS = {
|
||||
static: 'Ручной',
|
||||
domains: 'Ручной',
|
||||
json_url: 'JSON по URL',
|
||||
evobgp_community: 'EvoBGP community',
|
||||
} as const
|
||||
|
||||
export type IpListDbType = keyof typeof IP_LIST_SOURCE_LABELS
|
||||
|
||||
export function ipListSourceLabel(type: string): string {
|
||||
return (
|
||||
IP_LIST_SOURCE_LABELS[type as IpListDbType] ?? type
|
||||
)
|
||||
}
|
||||
|
||||
/** Manual lists are editable plaintext (legacy `domains` included). */
|
||||
export function isManualListType(type: string): boolean {
|
||||
return type === 'static' || type === 'domains'
|
||||
}
|
||||
|
||||
export const listEntryKindSchema = z.enum(['ip', 'cidr', 'hostname'])
|
||||
export type ListEntryKind = z.infer<typeof listEntryKindSchema>
|
||||
|
||||
export const listConfigItemSchema = z.object({
|
||||
kind: listEntryKindSchema,
|
||||
value: z.string().min(1),
|
||||
resolved_cidrs: z.array(z.string()).optional(),
|
||||
})
|
||||
export type ListConfigItem = z.infer<typeof listConfigItemSchema>
|
||||
|
||||
export const listEntryKindLabels: Record<ListEntryKind, string> = {
|
||||
ip: 'IP',
|
||||
cidr: 'CIDR',
|
||||
hostname: 'Домен',
|
||||
}
|
||||
|
||||
const IPV4 =
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/
|
||||
const IPV4_CIDR =
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\/(?:3[0-2]|[12]?\d)$/
|
||||
const IPV6 =
|
||||
/^(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$|^::(?:[0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^(?:[0-9a-fA-F]{0,4}:){1,7}:$/
|
||||
const IPV6_CIDR =
|
||||
/^(.+)\/(?:12[0-8]|1[01]\d|[1-9]?\d)$/
|
||||
const HOSTNAME =
|
||||
/^(?=.{1,253}$)(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))+$/
|
||||
|
||||
export function parseListEntry(raw: string): ListConfigItem {
|
||||
const value = raw.trim().replace(/\.$/, '')
|
||||
if (!value) throw new Error('Пустая строка')
|
||||
|
||||
if (IPV4_CIDR.test(value)) {
|
||||
return { kind: 'cidr', value }
|
||||
}
|
||||
if (IPV4.test(value)) {
|
||||
return { kind: 'ip', value }
|
||||
}
|
||||
|
||||
const v6Cidr = IPV6_CIDR.exec(value)
|
||||
if (v6Cidr && IPV6.test(v6Cidr[1]!)) {
|
||||
return { kind: 'cidr', value }
|
||||
}
|
||||
if (IPV6.test(value) && !value.includes('/')) {
|
||||
return { kind: 'ip', value }
|
||||
}
|
||||
|
||||
const host = value.toLowerCase()
|
||||
if (HOSTNAME.test(host) || (host.includes('.') && !host.includes(' '))) {
|
||||
if (/^https?:\/\//i.test(host)) {
|
||||
throw new Error('URL не является записью списка — создайте список «JSON по URL»')
|
||||
}
|
||||
return { kind: 'hostname', value: host }
|
||||
}
|
||||
|
||||
throw new Error(`Не удалось распознать: ${raw.trim()}`)
|
||||
}
|
||||
|
||||
/** Split plaintext paste into non-empty lines / tokens. */
|
||||
export function splitListPlaintext(text: string): string[] {
|
||||
return text
|
||||
.split(/[\n,;\s]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function normalizeItemToCidrs(
|
||||
item: ListConfigItem,
|
||||
resolvedHostCidrs?: string[],
|
||||
): string[] {
|
||||
if (item.kind === 'ip') {
|
||||
return [item.value.includes(':') ? `${item.value}/128` : `${item.value}/32`]
|
||||
}
|
||||
if (item.kind === 'cidr') {
|
||||
return [item.value]
|
||||
}
|
||||
return resolvedHostCidrs ?? []
|
||||
}
|
||||
|
||||
export function readManualItems(
|
||||
configJson: string,
|
||||
): ListConfigItem[] {
|
||||
let config: Record<string, unknown> = {}
|
||||
try {
|
||||
config = JSON.parse(configJson || '{}') as Record<string, unknown>
|
||||
} catch {
|
||||
config = {}
|
||||
}
|
||||
|
||||
if (Array.isArray(config.items)) {
|
||||
const out: ListConfigItem[] = []
|
||||
for (const raw of config.items) {
|
||||
const parsed = listConfigItemSchema.safeParse(raw)
|
||||
if (parsed.success) out.push(parsed.data)
|
||||
else if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
out.push(parseListEntry(raw))
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.length) return out
|
||||
}
|
||||
|
||||
if (Array.isArray(config.domains)) {
|
||||
return (config.domains as string[])
|
||||
.map((d) => d.trim())
|
||||
.filter(Boolean)
|
||||
.map((value) => ({ kind: 'hostname' as const, value: value.toLowerCase() }))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function guessListSourceFromInput(extra: string): 'static' | 'json_url' {
|
||||
const t = extra.trim()
|
||||
if (/^https?:\/\//i.test(t)) return 'json_url'
|
||||
return 'static'
|
||||
}
|
||||
|
||||
export const listEntriesBodySchema = z.object({
|
||||
values: z.array(z.string().min(1)).min(1),
|
||||
})
|
||||
|
||||
export const deleteListEntryBodySchema = z.object({
|
||||
value: z.string().min(1),
|
||||
})
|
||||
|
||||
export const createIpListSourceSchema = z.enum([
|
||||
'static',
|
||||
'json_url',
|
||||
'evobgp_community',
|
||||
])
|
||||
Reference in New Issue
Block a user