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
+18 -2
View File
@@ -137,9 +137,22 @@ function isValidIpCidr(v) {
return [a, b, c, d].every(n => n >= 0 && n <= 255) && p >= 0 && p <= 32;
}
function isValidDomain(v) {
if (!v || !v.trim()) return false;
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) {
const errors = {};
@@ -156,6 +169,8 @@ function validateServiceConfig(key, data) {
else if (parts[1]?.trim() && !isValidIp(parts[1].trim()))
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') {
port('port');
@@ -163,6 +178,7 @@ function validateServiceConfig(key, data) {
}
if (key === 'email') {
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 === '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}>
<TextInput value={data.dhcp_range} onChange={(v) => onChange({ ...data, dhcp_range: v })} placeholder="10.0.0.100,10.0.0.200,12h" />
</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" />
</Field>
</div>
@@ -317,7 +333,7 @@ function WireguardForm({ data, onChange, errors = {} }) {
function EmailForm({ data, onChange, errors = {} }) {
return (
<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" />
</Field>
<Field label="SMTP Port" hint="MTA-to-MTA (default 25)" error={errors.smtp_port}>