diff --git a/web/package-lock.json b/web/package-lock.json index 686e50e..a12062c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,7 +14,8 @@ "svelte-sonner": "^1.1.0", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3" }, "devDependencies": { "@internationalized/date": "^3.12.0", @@ -3137,9 +3138,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, "license": "MIT", - "optional": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/package.json b/web/package.json index a1c12c9..2db0ef4 100644 --- a/web/package.json +++ b/web/package.json @@ -39,6 +39,7 @@ "svelte-sonner": "^1.1.0", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3" } } diff --git a/web/src/lib/settings/settings-known.schema.ts b/web/src/lib/settings/settings-known.schema.ts new file mode 100644 index 0000000..5c95489 --- /dev/null +++ b/web/src/lib/settings/settings-known.schema.ts @@ -0,0 +1,81 @@ +import { z } from 'zod'; + +function isValidIPv4(value: string): boolean { + const parts = value.split('.'); + if (parts.length !== 4) return false; + for (const part of parts) { + if (!/^\d{1,3}$/.test(part)) return false; + if (part.length > 1 && part.startsWith('0')) return false; + const n = Number(part); + if (!Number.isInteger(n) || n < 0 || n > 255) return false; + } + return true; +} + +function isValidIPv6(value: string): boolean { + if (!/^[0-9A-Fa-f:.]+$/.test(value)) return false; + if ((value.match(/::/g) ?? []).length > 1) return false; + const hasCompression = value.includes('::'); + const [leftRaw, rightRaw = ''] = value.split('::'); + const left = leftRaw === '' ? [] : leftRaw.split(':'); + const right = rightRaw === '' ? [] : rightRaw.split(':'); + if (left.some((part) => part === '') || right.some((part) => part === '')) return false; + let segments = [...left, ...right]; + let ipv4TailSegments = 0; + const lastSegment = segments.at(-1); + if (lastSegment && lastSegment.includes('.')) { + if (!isValidIPv4(lastSegment)) return false; + segments = segments.slice(0, -1); + ipv4TailSegments = 2; + } + for (const segment of segments) { + if (!/^[0-9A-Fa-f]{1,4}$/.test(segment)) return false; + } + const totalSegments = segments.length + ipv4TailSegments; + if (hasCompression) return totalSegments < 8; + return totalSegments === 8; +} + +const optionalIPv4 = (label: string) => + z.string().refine((v) => v.trim() === '' || isValidIPv4(v.trim()), { + message: `Введите корректный IPv4 адрес (${label})` + }); + +const optionalIPv6 = (label: string) => + z.string().refine((v) => v.trim() === '' || isValidIPv6(v.trim()), { + message: `Введите корректный IPv6 адрес (${label})` + }); + +export const settingsKnownSchema = z.object({ + bird_router_id: optionalIPv4('router id'), + bird_local_ipv4: optionalIPv4('local IPv4'), + bird_local_ipv6: optionalIPv6('local IPv6'), + bird_local_asn: z + .string() + .refine((v) => v.trim() === '' || /^[1-9]\d*$/.test(v.trim()), { + message: 'ASN должен быть целым числом больше 0' + }), + bird_bgp_source_ipv4: optionalIPv4('BGP source IPv4'), + bird_bgp_source_ipv6: optionalIPv6('BGP source IPv6'), + revision_retention_minutes: z.string().refine( + (v) => { + const s = v.trim(); + if (s === '') return true; + const ttl = Number(s); + return /^\d+$/.test(s) && Number.isInteger(ttl) && ttl >= 15 && ttl <= 43200; + }, + { message: 'TTL ревизий должен быть целым числом от 15 до 43200 минут' } + ) +}); + +export type SettingsKnownForm = z.infer; + +export const emptySettingsKnownForm = (): SettingsKnownForm => ({ + bird_router_id: '', + bird_local_ipv4: '', + bird_local_ipv6: '', + bird_local_asn: '', + bird_bgp_source_ipv4: '', + bird_bgp_source_ipv6: '', + revision_retention_minutes: '' +}); diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index e42da12..9f46148 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -1,14 +1,22 @@