Skip to content

Commit 596887d

Browse files
feat: Add support for FQDN, IPv4, and IPv6 addresses in server settings
- Replace IPv4-only validation with comprehensive address validation - Support IPv4 addresses (e.g., 192.168.1.1) - Support IPv6 addresses including compressed format (e.g., ::1, 2001:db8::1) - Support FQDN/hostnames (e.g., server.example.com, localhost) - Update UI label from 'IP Address' to 'Host/IP Address' - Update placeholder text with examples for all supported formats - Update error messages to reflect new validation capabilities
1 parent 0676772 commit 596887d

File tree

1 file changed

+49
-7
lines changed

1 file changed

+49
-7
lines changed

src/app/_components/ServerForm.tsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,50 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
5353
void loadColorCodingSetting();
5454
}, []);
5555

56+
const validateServerAddress = (address: string): boolean => {
57+
const trimmed = address.trim();
58+
if (!trimmed) return false;
59+
60+
// IPv4 validation
61+
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
62+
if (ipv4Regex.test(trimmed)) {
63+
return true;
64+
}
65+
66+
// IPv6 validation (supports compressed format like ::1 and full format)
67+
// Matches: 2001:0db8:85a3:0000:0000:8a2e:0370:7334, ::1, 2001:db8::1, etc.
68+
// Also supports IPv4-mapped IPv6 addresses like ::ffff:192.168.1.1
69+
// Simplified validation: check for valid hex segments separated by colons
70+
const ipv6Pattern = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^(?:[0-9a-fA-F]{1,4}:)*::(?:[0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}$|^::(?:[0-9a-fA-F]{1,4}:)+[0-9a-fA-F]{1,4}$|^::ffff:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
71+
if (ipv6Pattern.test(trimmed)) {
72+
// Additional validation: ensure only one :: compression exists
73+
const compressionCount = (trimmed.match(/::/g) || []).length;
74+
if (compressionCount <= 1) {
75+
return true;
76+
}
77+
}
78+
79+
// FQDN/hostname validation (RFC 1123 compliant)
80+
// Allows letters, numbers, hyphens, dots; must start and end with alphanumeric
81+
// Max length 253 characters, each label max 63 characters
82+
const hostnameRegex = /^([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])?$/;
83+
if (hostnameRegex.test(trimmed) && trimmed.length <= 253) {
84+
// Additional check: each label (between dots) must be max 63 chars
85+
const labels = trimmed.split('.');
86+
if (labels.every(label => label.length > 0 && label.length <= 63)) {
87+
return true;
88+
}
89+
}
90+
91+
// Also allow simple hostnames without dots (like 'localhost')
92+
const simpleHostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$/;
93+
if (simpleHostnameRegex.test(trimmed) && trimmed.length <= 63) {
94+
return true;
95+
}
96+
97+
return false;
98+
};
99+
56100
const validateForm = (): boolean => {
57101
const newErrors: Partial<Record<keyof CreateServerData, string>> = {};
58102

@@ -61,12 +105,10 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
61105
}
62106

63107
if (!formData.ip.trim()) {
64-
newErrors.ip = 'IP address is required';
108+
newErrors.ip = 'Server address is required';
65109
} else {
66-
// Basic IP validation
67-
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
68-
if (!ipRegex.test(formData.ip)) {
69-
newErrors.ip = 'Please enter a valid IP address';
110+
if (!validateServerAddress(formData.ip)) {
111+
newErrors.ip = 'Please enter a valid IP address (IPv4/IPv6) or hostname';
70112
}
71113
}
72114

@@ -221,7 +263,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
221263

222264
<div>
223265
<label htmlFor="ip" className="block text-sm font-medium text-muted-foreground mb-1">
224-
IP Address *
266+
Host/IP Address *
225267
</label>
226268
<input
227269
type="text"
@@ -231,7 +273,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
231273
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
232274
errors.ip ? 'border-destructive' : 'border-border'
233275
}`}
234-
placeholder="e.g., 192.168.1.100"
276+
placeholder="e.g., 192.168.1.100, server.example.com, or 2001:db8::1"
235277
/>
236278
{errors.ip && <p className="mt-1 text-sm text-destructive">{errors.ip}</p>}
237279
</div>

0 commit comments

Comments
 (0)