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.\-_]*$/;