11import { z } from 'zod'
2+ import { isIP } from 'node:net'
3+
4+ /**
5+ * Validates public profile links.
6+ * Accepts http/https URLs with a real hostname, localhost, or a valid IP address.
7+ */
8+ const isValidHttpUrl = ( value : string ) => {
9+ try {
10+ const parsed = new URL ( value )
11+ if ( parsed . protocol !== 'http:' && parsed . protocol !== 'https:' ) {
12+ return false
13+ }
14+
15+ const hostname = parsed . hostname . toLowerCase ( )
16+ return hostname === 'localhost' || isIP ( hostname ) > 0 || / ^ [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] { 0 , 61 } [ a - z 0 - 9 ] ) ? (?: \. [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] { 0 , 61 } [ a - z 0 - 9 ] ) ? ) + $ / i. test ( hostname )
17+ } catch {
18+ return false
19+ }
20+ }
21+
22+ const httpUrlSchema = ( message : string ) =>
23+ z . string ( ) . trim ( ) . refine ( isValidHttpUrl , message )
224
325/**
426 * Profile update validation schema
@@ -21,21 +43,9 @@ export const profileUpdateSchema = z.object({
2143 . max ( 500 , 'Bio must not exceed 500 characters' )
2244 . optional ( )
2345 . or ( z . literal ( '' ) ) ,
24- github_url : z
25- . string ( )
26- . regex ( / ^ h t t p s ? : \/ \/ .+ / i, 'GitHub URL must be a valid URL' )
27- . optional ( )
28- . or ( z . literal ( '' ) ) ,
29- linkedin_url : z
30- . string ( )
31- . regex ( / ^ h t t p s ? : \/ \/ .+ / i, 'LinkedIn URL must be a valid URL' )
32- . optional ( )
33- . or ( z . literal ( '' ) ) ,
34- portfolio_url : z
35- . string ( )
36- . regex ( / ^ h t t p s ? : \/ \/ .+ / i, 'Portfolio URL must be a valid URL' )
37- . optional ( )
38- . or ( z . literal ( '' ) ) ,
46+ github_url : httpUrlSchema ( 'GitHub URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
47+ linkedin_url : httpUrlSchema ( 'LinkedIn URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
48+ portfolio_url : httpUrlSchema ( 'Portfolio URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
3949 skills : z
4050 . array ( z . string ( ) )
4151 . max ( 10 , 'Cannot have more than 10 skills' )
@@ -45,8 +55,7 @@ export const profileUpdateSchema = z.object({
4555 graduation_year : z . union ( [ z . number ( ) . int ( ) . min ( 1900 ) . max ( 2100 ) , z . string ( ) , z . null ( ) ] ) . optional ( ) ,
4656 phone : z . string ( ) . max ( 20 ) . optional ( ) . or ( z . literal ( '' ) ) ,
4757 address : z . string ( ) . max ( 200 ) . optional ( ) . or ( z . literal ( '' ) ) ,
48- avatar_url : z . string ( ) . url ( ) . optional ( ) . or ( z . literal ( '' ) ) ,
49- banner_url : z . string ( ) . regex ( / ^ h t t p s ? : \/ \/ .+ / i, 'Banner URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
58+ banner_url : httpUrlSchema ( 'Banner URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
5059 interests : z . array ( z . string ( ) ) . max ( 10 , 'Cannot have more than 10 interests' ) . optional ( ) ,
5160 first_name : z . string ( ) . max ( 50 ) . optional ( ) . or ( z . literal ( '' ) ) ,
5261 last_name : z . string ( ) . max ( 50 ) . optional ( ) . or ( z . literal ( '' ) ) ,
@@ -59,9 +68,10 @@ export const profileUpdateSchema = z.object({
5968 degree_type : z . string ( ) . optional ( ) . or ( z . literal ( '' ) ) ,
6069 graduation_month : z . string ( ) . optional ( ) . or ( z . literal ( '' ) ) ,
6170 roles : z . array ( z . string ( ) ) . optional ( ) ,
62- resume_url : z . string ( ) . url ( ) . optional ( ) . or ( z . literal ( '' ) ) ,
71+ avatar_url : httpUrlSchema ( 'Avatar URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
72+ resume_url : httpUrlSchema ( 'Resume URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
6373 has_experience : z . boolean ( ) . optional ( ) ,
64- twitter_url : z . string ( ) . regex ( / ^ h t t p s ? : \/ \/ . + / i , 'Twitter URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
74+ twitter_url : httpUrlSchema ( 'Twitter URL must be a valid URL' ) . optional ( ) . or ( z . literal ( '' ) ) ,
6575 emergency_contact_name : z . string ( ) . max ( 100 ) . optional ( ) . or ( z . literal ( '' ) ) ,
6676 emergency_contact_phone : z . string ( ) . max ( 20 ) . optional ( ) . or ( z . literal ( '' ) ) ,
6777 is_email_public : z . boolean ( ) . optional ( ) ,
0 commit comments