feat: domain validation for NTP servers and mail domain fields

- isValidDomain / isValidDomainOrIp helpers (RFC-compliant label regex)
- network.ntp_servers: each entry validated as hostname or IP; invalid entry shown in error message
- email.domain: validated as a proper domain name; blocks autosave until fixed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 09:39:59 -04:00
parent 15e009bd94
commit 4b994a5964
+21 -5
View File
@@ -137,9 +137,22 @@ function isValidIpCidr(v) {
return [a, b, c, d].every(n => n >= 0 && n <= 255) && p >= 0 && p <= 32; return [a, b, c, d].every(n => n >= 0 && n <= 255) && p >= 0 && p <= 32;
} }
const E_PORT = 'Must be 165535'; function isValidDomain(v) {
const E_IP = 'Must be a valid IP address'; if (!v || !v.trim()) return false;
const E_CIDR = 'Must be a valid IP/CIDR (e.g. 10.0.0.1/24)'; const s = v.trim();
if (s.length > 253) return false;
return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(s);
}
function isValidDomainOrIp(v) {
const s = (v || '').trim();
return isValidIp(s) || isValidDomain(s);
}
const E_PORT = 'Must be 165535';
const E_IP = 'Must be a valid IP address';
const E_CIDR = 'Must be a valid IP/CIDR (e.g. 10.0.0.1/24)';
const E_DOMAIN = 'Must be a valid domain (e.g. mail.example.com)';
function validateServiceConfig(key, data) { function validateServiceConfig(key, data) {
const errors = {}; const errors = {};
@@ -156,6 +169,8 @@ function validateServiceConfig(key, data) {
else if (parts[1]?.trim() && !isValidIp(parts[1].trim())) else if (parts[1]?.trim() && !isValidIp(parts[1].trim()))
errors.dhcp_range = `End IP is invalid`; errors.dhcp_range = `End IP is invalid`;
} }
const badNtp = (data.ntp_servers || []).find(s => !isValidDomainOrIp(s));
if (badNtp !== undefined) errors.ntp_servers = `"${badNtp}" is not a valid hostname or IP`;
} }
if (key === 'wireguard') { if (key === 'wireguard') {
port('port'); port('port');
@@ -163,6 +178,7 @@ function validateServiceConfig(key, data) {
} }
if (key === 'email') { if (key === 'email') {
port('smtp_port'); port('submission_port'); port('imap_port'); port('webmail_port'); port('smtp_port'); port('submission_port'); port('imap_port'); port('webmail_port');
if (data.domain && !isValidDomain(data.domain)) errors.domain = E_DOMAIN;
} }
if (key === 'calendar') port('port'); if (key === 'calendar') port('port');
if (key === 'files') { port('port'); port('manager_port'); } if (key === 'files') { port('port'); port('manager_port'); }
@@ -291,7 +307,7 @@ function NetworkForm({ data, onChange, errors = {} }) {
<Field label="DHCP Range" hint="e.g. 10.0.0.100,10.0.0.200,12h" error={errors.dhcp_range}> <Field label="DHCP Range" hint="e.g. 10.0.0.100,10.0.0.200,12h" error={errors.dhcp_range}>
<TextInput value={data.dhcp_range} onChange={(v) => onChange({ ...data, dhcp_range: v })} placeholder="10.0.0.100,10.0.0.200,12h" /> <TextInput value={data.dhcp_range} onChange={(v) => onChange({ ...data, dhcp_range: v })} placeholder="10.0.0.100,10.0.0.200,12h" />
</Field> </Field>
<Field label="NTP Servers"> <Field label="NTP Servers" hint="Hostnames or IPs" error={errors.ntp_servers}>
<TagList value={data.ntp_servers || []} onChange={(v) => onChange({ ...data, ntp_servers: v })} placeholder="0.pool.ntp.org" /> <TagList value={data.ntp_servers || []} onChange={(v) => onChange({ ...data, ntp_servers: v })} placeholder="0.pool.ntp.org" />
</Field> </Field>
</div> </div>
@@ -317,7 +333,7 @@ function WireguardForm({ data, onChange, errors = {} }) {
function EmailForm({ data, onChange, errors = {} }) { function EmailForm({ data, onChange, errors = {} }) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<Field label="Mail Domain"> <Field label="Mail Domain" error={errors.domain}>
<TextInput value={data.domain} onChange={(v) => onChange({ ...data, domain: v })} placeholder="mail.example.com" /> <TextInput value={data.domain} onChange={(v) => onChange({ ...data, domain: v })} placeholder="mail.example.com" />
</Field> </Field>
<Field label="SMTP Port" hint="MTA-to-MTA (default 25)" error={errors.smtp_port}> <Field label="SMTP Port" hint="MTA-to-MTA (default 25)" error={errors.smtp_port}>