@@ -23,20 +23,33 @@ const validateAge = (age: string) => {
2323 const ageNum = parseInt ( age ) ;
2424 return ! isNaN ( ageNum ) && ageNum > 0 && ageNum < 120 ;
2525} ;
26- const validateURL = ( url : string ) => {
26+
27+ const isValidLinkUrl = ( value : unknown ) : boolean => {
28+ if ( value === undefined || value === null ) return true ;
29+ if ( typeof value !== "string" ) return false ;
30+ const trimmed = value . trim ( ) ;
31+ if ( ! trimmed ) return true ;
32+ let parsed : URL ;
2733 try {
28- const parsedUrl = new URL ( url ) ;
29- // Check for valid hostname (at least one dot and valid characters)
30- if (
31- ! parsedUrl . hostname . includes ( "." ) ||
32- ! / ^ [ a - z A - Z 0 - 9 . - ] + $ / . test ( parsedUrl . hostname )
33- ) {
34- return false ;
35- }
36- return true ;
34+ parsed = new URL ( trimmed ) ;
3735 } catch {
3836 return false ;
3937 }
38+ return (
39+ ( parsed . protocol === "https:" || parsed . protocol === "http:" ) &&
40+ parsed . hostname . includes ( "." ) &&
41+ / ^ [ a - z A - Z 0 - 9 . - ] + $ / . test ( parsed . hostname )
42+ ) ;
43+ } ;
44+
45+ const isValidLinkDomain = ( value : unknown , domains : string [ ] ) : boolean => {
46+ if ( value === undefined || value === null ) return true ;
47+ if ( typeof value !== "string" || ! value . trim ( ) ) return true ;
48+ if ( ! isValidLinkUrl ( value ) ) return false ;
49+ const host = new URL ( value . trim ( ) ) . hostname
50+ . toLowerCase ( )
51+ . replace ( / ^ w w w \. / , "" ) ;
52+ return domains . some ( ( d ) => host === d || host . endsWith ( `.${ d } ` ) ) ;
4053} ;
4154const validateReferralCode = ( code : string ) => {
4255 const referralCodesEnv = process . env . VALID_REFERRAL_CODES || "" ;
@@ -216,7 +229,7 @@ export async function POST(request: Request) {
216229 message : "Too many requests. Please try again later." ,
217230 error : "Rate limit exceeded" ,
218231 } ,
219- { status : 429 }
232+ { status : 429 } ,
220233 ) ;
221234 }
222235
@@ -239,7 +252,11 @@ export async function POST(request: Request) {
239252 // the score + action, before any Firebase/Cloudinary/DB writes.
240253 const captcha = await verifyRecaptcha ( recaptcha_token , "register" ) ;
241254 if ( ! captcha . ok ) {
242- console . warn ( "[registration] reCAPTCHA rejected:" , captcha . reason , captcha . score ) ;
255+ console . warn (
256+ "[registration] reCAPTCHA rejected:" ,
257+ captcha . reason ,
258+ captcha . score ,
259+ ) ;
243260 return NextResponse . json (
244261 {
245262 message : "reCAPTCHA validation failed" ,
@@ -390,18 +407,23 @@ export async function POST(request: Request) {
390407 }
391408
392409 // Validate password (min 8 chars, 1 uppercase, 1 lowercase, 1 number, 1 special char)
393- const passwordRegex = / ^ (? = .* [ a - z ] ) (? = .* [ A - Z ] ) (? = .* \d ) (? = .* [ ^ A - Z a - z 0 - 9 ] ) .{ 8 , } $ / ;
410+ const passwordRegex =
411+ / ^ (? = .* [ a - z ] ) (? = .* [ A - Z ] ) (? = .* \d ) (? = .* [ ^ A - Z a - z 0 - 9 ] ) .{ 8 , } $ / ;
394412 if ( ! passwordRegex . test ( password ) ) {
395413 return NextResponse . json (
396414 {
397- message : "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character." ,
415+ message :
416+ "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character." ,
398417 error : "Weak password" ,
399418 } ,
400419 { status : 400 } ,
401420 ) ;
402421 }
403422
404- if ( data . github_link && ! validateURL ( data . github_link ) ) {
423+ if (
424+ data . github_link &&
425+ ! isValidLinkDomain ( data . github_link , [ "github.com" ] )
426+ ) {
405427 return NextResponse . json (
406428 {
407429 message : "Invalid GitHub profile URL format." ,
@@ -411,7 +433,10 @@ export async function POST(request: Request) {
411433 ) ;
412434 }
413435
414- if ( data . linkedin_link && ! validateURL ( data . linkedin_link ) ) {
436+ if (
437+ data . linkedin_link &&
438+ ! isValidLinkDomain ( data . linkedin_link , [ "linkedin.com" ] )
439+ ) {
415440 return NextResponse . json (
416441 {
417442 message : "Invalid LinkedIn profile URL format." ,
@@ -421,7 +446,7 @@ export async function POST(request: Request) {
421446 ) ;
422447 }
423448
424- if ( data . ctf_profile && ! validateURL ( data . ctf_profile ) ) {
449+ if ( data . ctf_profile && ! isValidLinkUrl ( data . ctf_profile ) ) {
425450 return NextResponse . json (
426451 {
427452 message : "Invalid CTF profile URL format." ,
@@ -431,7 +456,7 @@ export async function POST(request: Request) {
431456 ) ;
432457 }
433458
434- if ( data . portfolio_link && ! validateURL ( data . portfolio_link ) ) {
459+ if ( data . portfolio_link && ! isValidLinkUrl ( data . portfolio_link ) ) {
435460 return NextResponse . json (
436461 {
437462 message : "Invalid Portfolio URL format." ,
0 commit comments