diff --git a/apps/studio/components/interfaces/Auth/Auth.constants.ts b/apps/studio/components/interfaces/Auth/Auth.constants.ts index 1708eb6dce0..a970de63739 100644 --- a/apps/studio/components/interfaces/Auth/Auth.constants.ts +++ b/apps/studio/components/interfaces/Auth/Auth.constants.ts @@ -26,8 +26,18 @@ const customSchemeRegex = /^([a-zA-Z][a-zA-Z0-9+.-]*):(?:\/{1,3})?([a-zA-Z0-9_.- // Exclude simple domain names without protocol const excludeSimpleDomainRegex = /^[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$/ -// combine the above regexes -export const urlRegex = new RegExp( - `(?!${excludeSimpleDomainRegex.source})((${baseUrlRegex.source})|(${localhostRegex.source})|(${appRegex.source})|(${chromeExtensionRegex.source})|(${customSchemeRegex.source}))`, - 'i' -) +// combine the above regexes, with optional exclusion of options +// usage: urlRegex() or urlRegex({ excludeSimpleDomains: false }) +export function urlRegex( + options: { excludeSimpleDomains?: boolean } = { excludeSimpleDomains: true } +): RegExp { + const { excludeSimpleDomains } = options + const excludeSimpleDomainPart = excludeSimpleDomains + ? `(?!${excludeSimpleDomainRegex.source})` + : '' + + return new RegExp( + `${excludeSimpleDomainPart}((${baseUrlRegex.source})|(${localhostRegex.source})|(${appRegex.source})|(${chromeExtensionRegex.source})|(${customSchemeRegex.source}))`, + 'i' + ) +} diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx index 753baf9d8e2..f804a333cc3 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx @@ -570,7 +570,7 @@ const EXTERNAL_PROVIDER_AZURE = { then: (schema) => schema.required('Secret Value is required'), otherwise: (schema) => schema, }), - EXTERNAL_AZURE_URL: string().matches(urlRegex, 'Must be a valid URL').optional(), + EXTERNAL_AZURE_URL: string().matches(urlRegex(), 'Must be a valid URL').optional(), }), misc: { iconKey: 'microsoft-icon', @@ -812,7 +812,7 @@ const EXTERNAL_PROVIDER_GITLAB = { then: (schema) => schema.required('Client Secret is required'), otherwise: (schema) => schema, }), - EXTERNAL_GITLAB_URL: string().matches(urlRegex, 'Must be a valid URL').optional(), + EXTERNAL_GITLAB_URL: string().matches(urlRegex(), 'Must be a valid URL').optional(), }), misc: { iconKey: 'gitlab-icon', @@ -1007,8 +1007,8 @@ const EXTERNAL_PROVIDER_KEYCLOAK = { EXTERNAL_KEYCLOAK_URL: string().when('EXTERNAL_KEYCLOAK_ENABLED', { is: true, then: (schema) => - schema.matches(urlRegex, 'Must be a valid URL').required('Realm URL is required'), - otherwise: (schema) => schema.matches(urlRegex, 'Must be a valid URL'), + schema.matches(urlRegex(), 'Must be a valid URL').required('Realm URL is required'), + otherwise: (schema) => schema.matches(urlRegex(), 'Must be a valid URL'), }), }), misc: { @@ -1317,7 +1317,7 @@ const EXTERNAL_PROVIDER_WORKOS = { validationSchema: object().shape({ EXTERNAL_WORKOS_ENABLED: boolean().required(), EXTERNAL_WORKOS_URL: string() - .matches(urlRegex, 'Must be a valid URL') + .matches(urlRegex(), 'Must be a valid URL') .when('EXTERNAL_WORKOS_ENABLED', { is: true, then: (schema) => schema.required('WorkOS URL is required'), @@ -1408,7 +1408,7 @@ const PROVIDER_SAML = { }, validationSchema: object().shape({ SAML_ENABLED: boolean().required(), - SAML_EXTERNAL_URL: string().matches(urlRegex, 'Must be a valid URL').optional(), + SAML_EXTERNAL_URL: string().matches(urlRegex(), 'Must be a valid URL').optional(), SAML_ALLOW_ENCRYPTED_ASSERTIONS: boolean().optional(), }), misc: { diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx index adbbbac9e04..92fdbf8fde1 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/AddNewURLModal.tsx @@ -40,7 +40,7 @@ export const AddNewURLModal = ({ visible, allowList, onClose }: AddNewURLModalPr value: z .string() .min(1, 'Please provide a value') - .regex(urlRegex, 'Please provide a valid URL') + .regex(urlRegex(), 'Please provide a valid URL') .refine((value) => !allowList.includes(value), { message: 'URL already exists in the allow list', }), diff --git a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx index f80f0f548fa..5dce9fcf113 100644 --- a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx +++ b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx @@ -76,7 +76,7 @@ const SmtpForm = () => { }, then: (schema) => schema - .matches(urlRegex, 'Must be a valid URL or IP address') + .matches(urlRegex({ excludeSimpleDomains: false }), 'Must be a valid URL or IP address') .required('Host URL is required.'), otherwise: (schema) => schema, }), diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index caa6249c62d..32365416b10 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -66,7 +66,7 @@ const httpRequestSchema = z.object({ .string() .trim() .min(1, 'Please provide a URL') - .regex(urlRegex, 'Please provide a valid URL') + .regex(urlRegex(), 'Please provide a valid URL') .refine((value) => value.startsWith('http'), 'Please include HTTP/HTTPs to your URL'), timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000), httpHeaders: z.array(z.object({ name: z.string(), value: z.string() })), diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx index a2d1e8a53f9..8f18c1b1317 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx @@ -45,7 +45,7 @@ const FORM_ID = 'log-drain-destination-form' const formUnion = z.discriminatedUnion('type', [ z.object({ type: z.literal('webhook'), - url: z.string().regex(urlRegex, 'Endpoint URL is required and must be a valid URL'), + url: z.string().regex(urlRegex(), 'Endpoint URL is required and must be a valid URL'), http: z.enum(['http1', 'http2']), gzip: z.boolean(), headers: z.record(z.string(), z.string()).optional(), diff --git a/apps/studio/tests/components/Auth/Auth.constants.test.ts b/apps/studio/tests/components/Auth/Auth.constants.test.ts index 0f7f5ff85b4..5f179a4b545 100644 --- a/apps/studio/tests/components/Auth/Auth.constants.test.ts +++ b/apps/studio/tests/components/Auth/Auth.constants.test.ts @@ -22,20 +22,30 @@ describe('Auth.constants: urlRegex', () => { ] validUrls.forEach((url) => { - expect(urlRegex.test(url)).toBe(true) + expect(urlRegex().test(url)).toBe(true) }) }) it('should not match invalid URLs', () => { const invalidUrls = ['supabase', 'mailto:test@gmail.com', 'hello world.com', 'email@domain.com'] - const failingInvalidUrls = invalidUrls.filter((url) => urlRegex.test(url)) + const failingInvalidUrls = invalidUrls.filter((url) => urlRegex().test(url)) if (failingInvalidUrls.length > 0) { console.log('Failing invalid URLs:', failingInvalidUrls) } invalidUrls.forEach((url) => { - expect(urlRegex.test(url)).toBe(false) + expect(urlRegex().test(url)).toBe(false) }) }) + + it('should not match simple domain URLs when excludeSimpleDomains is true', () => { + const simpleDomainUrl = 'smtp-pulse.com' + expect(urlRegex({ excludeSimpleDomains: true }).test(simpleDomainUrl)).toBe(false) + }) + + it('should match simple domain URLs when excludeSimpleDomains is false', () => { + const simpleDomainUrl = 'smtp-pulse.com' + expect(urlRegex({ excludeSimpleDomains: false }).test(simpleDomainUrl)).toBe(true) + }) })