From cc09d04a66679a8b050900b16f29deca24f85bf1 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:36:04 +0100 Subject: [PATCH 1/7] setup edge validators --- .../EdgeSetupPage/steps/SetupEdgeComponentStep.tsx | 12 +++++++++--- web/src/shared/patterns.ts | 5 +++-- web/src/shared/validate.ts | 13 +++++++++---- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx index b6049b3bd8..e8840e0468 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx @@ -9,7 +9,7 @@ import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedB import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; -import { validateIpOrDomain } from '../../../shared/validators'; +import { Validate } from '../../../shared/validate'; import { EdgeSetupStep } from '../types'; import { useEdgeWizardStore } from '../useEdgeWizardStore'; @@ -53,11 +53,17 @@ export const SetupEdgeComponentStep = () => { ip_or_domain: z .string() .min(1, m.edge_setup_component_error_ip_or_domain_required()) - .refine((val) => validateIpOrDomain(val, false, true)), + .refine((val) => + Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + false, + ), + ), grpc_port: z .number() .min(1, m.edge_setup_component_error_grpc_port_required()) - .max(65535, m.edge_setup_component_error_grpc_port_max()), + .refine((val) => Validate.any(val.toString(), [Validate.Port], false)), }), [], ); diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index b955d40f44..c9272e5905 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -67,11 +67,12 @@ export const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; export const ipv4WithPortPattern = /^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$/; export const ipv4WithCIDRPattern = /^(\d{1,3}\.){3}\d{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/; export const domainPattern = - /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; - + /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\])$/; export const domainWithPortPattern = /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9\-.]){1,61}(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3})):[0-9]{1,5}$/; +export const hostnamePattern = /^[A-Za-z]([A-Za-z0-9-]*[A-Za-z0-9])?$/; + export const patternSafeUsernameCharacters = /^[a-zA-Z0-9]+[a-zA-Z0-9.\-_]*$/; export const patternLoginCharacters = /^[a-zA-Z0-9]+[a-zA-Z0-9.\-_@]*$/; diff --git a/web/src/shared/validate.ts b/web/src/shared/validate.ts index 19208eb05a..5b201b62af 100644 --- a/web/src/shared/validate.ts +++ b/web/src/shared/validate.ts @@ -1,6 +1,7 @@ import ipaddr from 'ipaddr.js'; import { domainPattern, + hostnamePattern, ipv4Pattern, ipv4WithCIDRPattern, ipv4WithPortPattern, @@ -48,11 +49,11 @@ export const Validate = { } return true; }, - CIDRv4: (ip: string): boolean => { + CIDRv4: (ip: string, allow_zero: boolean = false): boolean => { if (!ipv4WithCIDRPattern.test(ip)) { return false; } - if (ip.endsWith('/0')) { + if (ip.endsWith('/0') && !allow_zero) { return false; } if (!ipaddr.IPv4.isValidCIDR(ip)) { @@ -60,8 +61,8 @@ export const Validate = { } return true; }, - CIDRv6: (ip: string): boolean => { - if (ip.endsWith('/0')) { + CIDRv6: (ip: string, allow_zero: boolean = false): boolean => { + if (ip.endsWith('/0') && !allow_zero) { return false; } if (!ipaddr.IPv6.isValidCIDR(ip)) { @@ -100,6 +101,10 @@ export const Validate = { } return false; }, + // Single-label hostname e.g. "localhost" + Hostname: (hostname: string): boolean => { + return hostnamePattern.test(hostname); + }, any: ( value: string | undefined, validators: Array<(val: string) => boolean>, From 7d709b355bb201ea8ba824d99533045daccd5f27 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:19:10 +0100 Subject: [PATCH 2/7] edit location validator --- .../EditLocationPage/EditLocationPage.tsx | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/web/src/pages/EditLocationPage/EditLocationPage.tsx b/web/src/pages/EditLocationPage/EditLocationPage.tsx index 7ceee535e7..c6cf92bc5e 100644 --- a/web/src/pages/EditLocationPage/EditLocationPage.tsx +++ b/web/src/pages/EditLocationPage/EditLocationPage.tsx @@ -32,7 +32,7 @@ import { canUseBusinessFeature, canUseEnterpriseFeature, } from '../../shared/utils/license'; -import { validateIpList, validateIpOrDomainList } from '../../shared/validators'; +import { Validate } from '../../shared/validate'; export const EditLocationPage = () => { const { locationId: paramsId } = useParams({ @@ -76,17 +76,51 @@ const formSchema = z .string(m.form_error_required()) .trim() .min(1, m.form_error_required()) - .refine((value) => validateIpList(value, ',', true), m.form_error_invalid()), - endpoint: z.string(m.form_error_required()).trim().min(1, m.form_error_required()), + .refine( + (val) => Validate.any(val, [Validate.CIDRv4, Validate.CIDRv6], true), + m.form_error_invalid(), + ), + endpoint: z + .string(m.form_error_required()) + .trim() + .min(1, m.form_error_required()) + .refine((val) => + Validate.any(val, [ + Validate.IPv4, + Validate.IPv6, + Validate.Domain, + Validate.Hostname, + ]), + ), port: z.number(m.form_error_required()).max(65535, m.form_error_port_max()), - allowed_ips: z.string(m.form_error_required()).trim(), + allowed_ips: z + .string() + .trim() + .nullable() + .refine((val) => { + if (!val) return true; + return Validate.any( + val, + [ + Validate.IPv4, + Validate.IPv6, + (v) => Validate.CIDRv4(v, true), + (v) => Validate.CIDRv6(v, true), + ], + true, + ); + }, m.form_error_invalid()), dns: z .string() .trim() .nullable() .refine((val) => { if (!val) return true; - return validateIpOrDomainList(val, ',', true, true); + return Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + true, + ); }), peer_disconnect_threshold: z.number().nullable(), keepalive_interval: z @@ -237,6 +271,7 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => { ...omit(clone, ['firewall']), allow_all_groups: clone.allow_all_groups, allowed_groups: clone.allowed_groups, + allowed_ips: clone.allowed_ips ?? '', acl_default_allow: clone.firewall === LocationFirewall.Allow, acl_enabled: !(clone.firewall === LocationFirewall.Disabled), peer_disconnect_threshold: peerDisconnectThreshold, From 08d7f2c236534294c2c6dbefb0f13dbad81ade0a Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:30:47 +0100 Subject: [PATCH 3/7] remove validateIpOrDomain --- .../EdgeSetupPage/steps/SetupEdgeComponentStep.tsx | 2 +- .../steps/SetupGatewayComponentStep.tsx | 10 ++++++++-- .../steps/MigrationWizardEdgeComponentStep.tsx | 10 ++++++++-- .../SetupPage/initial/steps/SetupEdgeComponentStep.tsx | 10 ++++++++-- .../settings/SettingsSmtpPage/SettingsSmtpPage.tsx | 4 ++-- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx index e8840e0468..5fbc541efa 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx @@ -63,7 +63,7 @@ export const SetupEdgeComponentStep = () => { grpc_port: z .number() .min(1, m.edge_setup_component_error_grpc_port_required()) - .refine((val) => Validate.any(val.toString(), [Validate.Port], false)), + .max(65535, m.edge_setup_component_error_grpc_port_max()), }), [], ); diff --git a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx index 0487f66e9a..e1658daa43 100644 --- a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx +++ b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx @@ -12,7 +12,7 @@ import { formChangeLogic } from '../../../shared/formLogic'; import { GatewaySetupStep } from '../types'; import { useGatewayWizardStore } from '../useGatewayWizardStore'; import './style.scss'; -import { validateIpOrDomain } from '../../../shared/validators'; +import { Validate } from '../../../shared/validate'; type FormFields = StoreValues; @@ -54,7 +54,13 @@ export const SetupGatewayComponentStep = () => { ip_or_domain: z .string() .min(1, m.edge_setup_component_error_ip_or_domain_required()) - .refine((val) => validateIpOrDomain(val, false, true)), + .refine((val) => + Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + false, + ) + ), grpc_port: z .number() .min(1, m.edge_setup_component_error_grpc_port_required()) diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx index 9f12c0270e..22c1e233a8 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx @@ -10,8 +10,8 @@ import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedB import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; -import { validateIpOrDomain } from '../../../shared/validators'; import { useMigrationWizardStore } from '../store/useMigrationWizardStore'; +import { Validate } from '../../../shared/validate'; type FormFields = StoreValues; @@ -41,7 +41,13 @@ export const MigrationWizardEdgeComponentStep = () => { ip_or_domain: z .string() .min(1, m.edge_setup_component_error_ip_or_domain_required()) - .refine((val) => validateIpOrDomain(val, false, true)), + .refine((val) => + Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + false, + ) + ), grpc_port: z .number() .min(1, m.edge_setup_component_error_grpc_port_required()) diff --git a/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx b/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx index 10ffae620a..f2e7e4a085 100644 --- a/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx @@ -9,9 +9,9 @@ import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/Siz import { ThemeSpacing } from '../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../shared/form'; import { formChangeLogic } from '../../../../shared/formLogic'; -import { validateIpOrDomain } from '../../../../shared/validators'; import { SetupPageStep } from '../types'; import { useSetupWizardStore } from '../useSetupWizardStore'; +import { Validate } from '../../../../shared/validate'; type FormFields = StoreValues; @@ -47,7 +47,13 @@ export const SetupEdgeComponentStep = () => { ip_or_domain: z .string() .min(1, m.edge_setup_component_error_ip_or_domain_required()) - .refine((val) => validateIpOrDomain(val, false, true)), + .refine((val) => + Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + false, + ) + ), grpc_port: z .number() .min(1, m.edge_setup_component_error_grpc_port_required()) diff --git a/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx b/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx index ac3c8f7f8e..6a8fda8cce 100644 --- a/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx +++ b/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx @@ -30,9 +30,9 @@ import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; import { useApp } from '../../../shared/hooks/useApp'; import { patternValidEmail } from '../../../shared/patterns'; import { getSettingsQueryOptions } from '../../../shared/query'; -import { validateIpOrDomain } from '../../../shared/validators'; import { configuredBadge, notConfiguredBadge } from '../SettingsIndexPage/types'; import { SendTestEmailModal } from './SendTestEmailModal'; +import { Validate } from '../../../shared/validate'; const breadcrumbsLinks = [ { .string() .trim() .min(1, m.form_error_required()) - .refine((val) => (!val ? true : validateIpOrDomain(val, false, true))), + .refine((val) => (!val ? true : Validate.any(val,[Validate.IPv4,Validate.IPv6,Validate.Domain,Validate.Hostname],false))), smtp_port: z.number(m.form_error_required()).max(65535, m.form_error_port_max()), smtp_password: z.string().trim(), smtp_user: z.string().trim(), From 28cc09ebd0f3abbd2933aae076170282db3d8d2f Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:52:34 +0100 Subject: [PATCH 4/7] remove unused functions --- web/src/shared/validators.ts | 112 +---------------------------------- 1 file changed, 1 insertion(+), 111 deletions(-) diff --git a/web/src/shared/validators.ts b/web/src/shared/validators.ts index 4f0a567b11..f471670122 100644 --- a/web/src/shared/validators.ts +++ b/web/src/shared/validators.ts @@ -1,113 +1,7 @@ import ipaddr from 'ipaddr.js'; import { z } from 'zod'; import { m } from '../paraglide/messages'; -import { patternStrictIpV4, patternValidWireguardKey } from './patterns'; -import { Validate } from './validate'; - -export const validateWireguardPublicKey = () => - z - .string(m.form_error_required()) - .length( - 44, - m.form_error_len({ - length: 44, - }), - ) - .regex(patternValidWireguardKey, m.form_error_invalid()); - -// Returns false when invalid -export const validateIpOrDomain = ( - val: string, - allowMask = false, - allowIPv6 = false, -): boolean => { - const hasLetter = /\p{L}/u.test(val); - const hasColon = /:/.test(val); - if (!hasLetter || hasColon) { - return (allowIPv6 && validateIPv6(val, allowMask)) || validateIPv4(val, allowMask); - } else { - return Validate.Domain(val); - } -}; - -// Returns false when invalid -export const validateIpList = ( - val: string, - splitWith = ',', - allowMasks = false, -): boolean => { - return val - .replace(' ', '') - .split(splitWith) - .every((el) => { - if (!el.includes('/') && allowMasks) return false; - return validateIPv4(el, allowMasks) || validateIPv6(el, allowMasks); - }); -}; - -// Returns false when invalid -export const validateIpOrDomainList = ( - val: string, - splitWith = ',', - allowMasks = false, - allowIPv6 = false, -): boolean => { - const trimmed = val.replace(' ', ''); - const split = trimmed.split(splitWith); - for (const value of split) { - if ( - !validateIPv4(value, allowMasks) && - !Validate.Domain(value) && - (!allowIPv6 || !validateIPv6(value, allowMasks)) - ) { - return false; - } - } - return true; -}; - -// Returns false when invalid -export const validateIPv4 = (ip: string, allowMask = false): boolean => { - if (allowMask) { - if (ip.endsWith('/0')) { - return false; - } - if (ip.includes('/')) { - return ipaddr.IPv4.isValidCIDR(ip); - } - } - const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; - const ipv4WithPortPattern = /^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$/; - if (!ipv4Pattern.test(ip) && !ipv4WithPortPattern.test(ip)) { - return false; - } - - if (ipv4WithPortPattern.test(ip)) { - const [address, port] = ip.split(':'); - ip = address; - if (!validatePort(port)) { - return false; - } - } - - return ipaddr.IPv4.isValid(ip); -}; - -export const validateIPv6 = (ip: string, allowMask = false): boolean => { - if (allowMask) { - if (ip.endsWith('/0')) { - return false; - } - if (ip.includes('/')) { - return ipaddr.IPv6.isValidCIDR(ip); - } - } - return ipaddr.IPv6.isValid(ip); -}; - -export const validatePort = (val: string): boolean => { - return parsePortNumber(val) !== null; -}; +import { patternStrictIpV4 } from './patterns'; type ParsedAclPortToken = [number] | [number, number]; @@ -174,10 +68,6 @@ const parseAclPorts = (value: string): ParsedAclPortToken[] | null => { return parsedTokens; }; -export const numericString = (val: string) => /^\d+$/.test(val); - -export const numericStringFloat = (val: string) => /^\d*\.?\d+$/.test(val); - export const aclPortsValidator = z .string() .refine((value: string) => parseAclPorts(value) !== null, m.form_error_invalid()); From c70703bd08ec169996ee3d8177c38ec20ff8cadd Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:52:41 +0100 Subject: [PATCH 5/7] format --- .../steps/SetupGatewayComponentStep.tsx | 2 +- .../steps/MigrationWizardEdgeComponentStep.tsx | 4 ++-- .../initial/steps/SetupEdgeComponentStep.tsx | 4 ++-- .../settings/SettingsSmtpPage/SettingsSmtpPage.tsx | 12 ++++++++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx index e1658daa43..77ee8af08a 100644 --- a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx +++ b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx @@ -59,7 +59,7 @@ export const SetupGatewayComponentStep = () => { val, [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], false, - ) + ), ), grpc_port: z .number() diff --git a/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx b/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx index 22c1e233a8..8283e4230e 100644 --- a/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx +++ b/web/src/pages/MigrationWizardPage/steps/MigrationWizardEdgeComponentStep.tsx @@ -10,8 +10,8 @@ import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedB import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; -import { useMigrationWizardStore } from '../store/useMigrationWizardStore'; import { Validate } from '../../../shared/validate'; +import { useMigrationWizardStore } from '../store/useMigrationWizardStore'; type FormFields = StoreValues; @@ -46,7 +46,7 @@ export const MigrationWizardEdgeComponentStep = () => { val, [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], false, - ) + ), ), grpc_port: z .number() diff --git a/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx b/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx index f2e7e4a085..fed43b5b8c 100644 --- a/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx @@ -9,9 +9,9 @@ import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/Siz import { ThemeSpacing } from '../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../shared/form'; import { formChangeLogic } from '../../../../shared/formLogic'; +import { Validate } from '../../../../shared/validate'; import { SetupPageStep } from '../types'; import { useSetupWizardStore } from '../useSetupWizardStore'; -import { Validate } from '../../../../shared/validate'; type FormFields = StoreValues; @@ -52,7 +52,7 @@ export const SetupEdgeComponentStep = () => { val, [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], false, - ) + ), ), grpc_port: z .number() diff --git a/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx b/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx index 6a8fda8cce..9910bcacbb 100644 --- a/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx +++ b/web/src/pages/settings/SettingsSmtpPage/SettingsSmtpPage.tsx @@ -30,9 +30,9 @@ import { ModalName } from '../../../shared/hooks/modalControls/modalTypes'; import { useApp } from '../../../shared/hooks/useApp'; import { patternValidEmail } from '../../../shared/patterns'; import { getSettingsQueryOptions } from '../../../shared/query'; +import { Validate } from '../../../shared/validate'; import { configuredBadge, notConfiguredBadge } from '../SettingsIndexPage/types'; import { SendTestEmailModal } from './SendTestEmailModal'; -import { Validate } from '../../../shared/validate'; const breadcrumbsLinks = [ { .string() .trim() .min(1, m.form_error_required()) - .refine((val) => (!val ? true : Validate.any(val,[Validate.IPv4,Validate.IPv6,Validate.Domain,Validate.Hostname],false))), + .refine((val) => + !val + ? true + : Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + false, + ), + ), smtp_port: z.number(m.form_error_required()).max(65535, m.form_error_port_max()), smtp_password: z.string().trim(), smtp_user: z.string().trim(), From 43b5f7e8c53ad9f6b95b73397ab8f4a4948e2da8 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:00:38 +0100 Subject: [PATCH 6/7] adjust wizard --- .../steps/AddLocationInternalVpnStep.tsx | 20 ++++++++++++++++++- .../steps/AddLocationStartStep.tsx | 8 +++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx b/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx index 1c325ce6c3..8402b74be2 100644 --- a/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx +++ b/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx @@ -22,7 +22,23 @@ const formSchema = z.object({ (value) => Validate.any(value, [Validate.CIDRv4, Validate.CIDRv6], true), m.form_error_invalid(), ), - allowed_ips: z.string(m.form_error_required()).trim(), + allowed_ips: z + .string() + .trim() + .nullable() + .refine((val) => { + if (!val) return true; + return Validate.any( + val, + [ + Validate.IPv4, + Validate.IPv6, + (v) => Validate.CIDRv4(v, true), + (v) => Validate.CIDRv6(v, true), + ], + true, + ); + }, m.form_error_invalid()), dns: z.string().nullable(), }); @@ -48,6 +64,7 @@ export const AddLocationInternalVpnStep = () => { onSubmit: ({ value }) => { useAddLocationStore.setState({ ...value, + allowed_ips: value.allowed_ips ?? '', activeStep: AddLocationPageStep.NetworkSettings, }); }, @@ -96,6 +113,7 @@ export const AddLocationInternalVpnStep = () => { useAddLocationStore.setState({ activeStep: AddLocationPageStep.Start, ...form.state.values, + allowed_ips: form.state.values.allowed_ips ?? '', }); }} /> diff --git a/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx b/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx index 0f17dfd3d0..2219085366 100644 --- a/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx +++ b/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx @@ -21,7 +21,13 @@ const formSchema = z.object({ .trim() .min(1, m.form_error_required()) .refine( - (value) => Validate.any(value, [Validate.IPv4, Validate.IPv6, Validate.Domain]), + (value) => + Validate.any(value, [ + Validate.IPv4, + Validate.IPv6, + Validate.Domain, + Validate.Hostname, + ]), m.form_error_invalid(), ), port: z.number(m.form_error_required()).max(65535, m.form_error_port_max()), From 1167169a676045f41f8f2760e1ebf2d3d6428c3f Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:25:07 +0100 Subject: [PATCH 7/7] add missing validators --- .../steps/AddLocationInternalVpnStep.tsx | 13 ++++- .../steps/AddLocationStartStep.tsx | 5 +- .../steps/AutoAdoptionVpnSettingsStep.tsx | 54 ++++++++++++++++--- web/src/shared/patterns.ts | 2 +- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx b/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx index 8402b74be2..4d4bc5295c 100644 --- a/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx +++ b/web/src/pages/AddLocationPage/steps/AddLocationInternalVpnStep.tsx @@ -39,7 +39,18 @@ const formSchema = z.object({ true, ); }, m.form_error_invalid()), - dns: z.string().nullable(), + dns: z + .string() + .trim() + .nullable() + .refine((val) => { + if (!val) return true; + return Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + true, + ); + }), }); type FormFields = z.infer; diff --git a/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx b/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx index 2219085366..fda2c0d5d8 100644 --- a/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx +++ b/web/src/pages/AddLocationPage/steps/AddLocationStartStep.tsx @@ -30,7 +30,10 @@ const formSchema = z.object({ ]), m.form_error_invalid(), ), - port: z.number(m.form_error_required()).max(65535, m.form_error_port_max()), + port: z + .number(m.form_error_required()) + .min(1, m.form_min_value({ value: 1 })) + .max(65535, m.form_error_port_max()), }); type FormFields = z.infer; diff --git a/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionVpnSettingsStep.tsx b/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionVpnSettingsStep.tsx index 461e4900ec..460076c1be 100644 --- a/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionVpnSettingsStep.tsx +++ b/web/src/pages/SetupPage/autoAdoption/steps/AutoAdoptionVpnSettingsStep.tsx @@ -22,7 +22,13 @@ const formSchema = z.object({ .trim() .min(1, m.form_error_required()) .refine( - (value) => Validate.any(value, [Validate.IPv4, Validate.IPv6, Validate.Domain]), + (value) => + Validate.any(value, [ + Validate.IPv4, + Validate.IPv6, + Validate.Domain, + Validate.Hostname, + ]), m.initial_setup_auto_adoption_vpn_error_invalid_value(), ), vpn_wireguard_port: z @@ -37,8 +43,35 @@ const formSchema = z.object({ (value) => Validate.any(value, [Validate.CIDRv4, Validate.CIDRv6], true), m.initial_setup_auto_adoption_vpn_error_invalid_value(), ), - vpn_allowed_ips: z.string().trim(), - vpn_dns_server_ip: z.string().trim(), + vpn_allowed_ips: z + .string() + .trim() + .nullable() + .refine((val) => { + if (!val) return true; + return Validate.any( + val, + [ + Validate.IPv4, + Validate.IPv6, + (v) => Validate.CIDRv4(v, true), + (v) => Validate.CIDRv6(v, true), + ], + true, + ); + }, m.form_error_invalid()), + vpn_dns_server_ip: z + .string() + .trim() + .nullable() + .refine((val) => { + if (!val) return true; + return Validate.any( + val, + [Validate.IPv4, Validate.IPv6, Validate.Domain, Validate.Hostname], + true, + ); + }), }); type FormFields = z.infer; @@ -72,8 +105,13 @@ export const AutoAdoptionVpnSettingsStep = () => { onChange: formSchema, }, onSubmit: ({ value }) => { - useAutoAdoptionSetupWizardStore.setState(value); - setVpnSettings(value); + const storeValue = { + ...value, + vpn_allowed_ips: value.vpn_allowed_ips ?? '', + vpn_dns_server_ip: value.vpn_dns_server_ip ?? '', + }; + useAutoAdoptionSetupWizardStore.setState(storeValue); + setVpnSettings(storeValue); }, }); @@ -144,7 +182,11 @@ export const AutoAdoptionVpnSettingsStep = () => { variant="outlined" text={m.initial_setup_controls_back()} onClick={() => { - useAutoAdoptionSetupWizardStore.setState(form.state.values); + useAutoAdoptionSetupWizardStore.setState({ + ...form.state.values, + vpn_allowed_ips: form.state.values.vpn_allowed_ips ?? '', + vpn_dns_server_ip: form.state.values.vpn_dns_server_ip ?? '', + }); setActiveStep(AutoAdoptionSetupStep.UrlSettings); }} /> diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index c9272e5905..975879951f 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -71,7 +71,7 @@ export const domainPattern = export const domainWithPortPattern = /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9\-.]){1,61}(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3})):[0-9]{1,5}$/; -export const hostnamePattern = /^[A-Za-z]([A-Za-z0-9-]*[A-Za-z0-9])?$/; +export const hostnamePattern = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/; export const patternSafeUsernameCharacters = /^[a-zA-Z0-9]+[a-zA-Z0-9.\-_]*$/;