@@ -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 = / ^ (?: (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) \. ) { 3 } (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 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 - 9 a - f A - F ] { 1 , 4 } : ) { 7 } [ 0 - 9 a - f A - F ] { 1 , 4 } $ | ^ : : 1 $ | ^ : : $ | ^ (?: [ 0 - 9 a - f A - F ] { 1 , 4 } : ) * : : (?: [ 0 - 9 a - f A - F ] { 1 , 4 } : ) * [ 0 - 9 a - f A - F ] { 1 , 4 } $ | ^ (?: [ 0 - 9 a - f A - F ] { 1 , 4 } : ) * : : [ 0 - 9 a - f A - F ] { 1 , 4 } $ | ^ : : (?: [ 0 - 9 a - f A - F ] { 1 , 4 } : ) + [ 0 - 9 a - f A - F ] { 1 , 4 } $ | ^ : : f f f f : (?: (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) \. ) { 3 } (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) $ | ^ (?: [ 0 - 9 a - f A - F ] { 1 , 4 } : ) { 1 , 4 } : (?: (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) \. ) { 3 } (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 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 - z A - Z 0 - 9 ] ( [ a - z A - Z 0 - 9 \- ] { 0 , 61 } [ a - z A - Z 0 - 9 ] ) ? \. ) * [ a - z A - Z 0 - 9 ] ( [ a - z A - Z 0 - 9 \- ] { 0 , 61 } [ a - z A - Z 0 - 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 - z A - Z 0 - 9 ] ( [ a - z A - Z 0 - 9 \- ] { 0 , 61 } [ a - z A - Z 0 - 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 = / ^ (?: (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 0 - 9 ] [ 0 - 9 ] ? ) \. ) { 3 } (?: 2 5 [ 0 - 5 ] | 2 [ 0 - 4 ] [ 0 - 9 ] | [ 0 1 ] ? [ 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