diff --git a/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx b/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx index 73d736d80a83b..694d94dd4ee60 100644 --- a/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx @@ -1,21 +1,21 @@ -import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Toggle } from 'ui' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { X } from 'lucide-react' import { toast } from 'sonner' +import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Toggle } from 'ui' import { useConsentState } from 'common' +import { LOCAL_STORAGE_KEYS } from 'common/constants/local-storage' import Panel from 'components/ui/Panel' import { useSendResetMutation } from 'data/telemetry/send-reset-mutation' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' -import { LOCAL_STORAGE_KEYS } from 'common/constants/local-storage' export const TermsUpdateBanner = () => { - const [termsUpdateAcknowledged, setTermsUpdateAcknowledged] = useLocalStorageQuery( + const [termsUpdateAcknowledged, setTermsUpdateAcknowledged, { isSuccess }] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.TERMS_OF_SERVICE_ACKNOWLEDGED, false ) - if (termsUpdateAcknowledged) return null + if (!isSuccess || termsUpdateAcknowledged) return null return ( diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx index 1931a49b40861..0137a0685f3b5 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx @@ -19,11 +19,6 @@ const PROVIDER_EMAIL = { description: 'This will enable Email based signup and login for your application', type: 'boolean', }, - MAILER_AUTOCONFIRM: { - title: 'Confirm email', - description: `Users will need to confirm their email address before signing in for the first time.`, - type: 'boolean', - }, MAILER_SECURE_EMAIL_CHANGE_ENABLED: { title: 'Secure email change', description: `Users will be required to confirm any email change on both the old email address and new email address. diff --git a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx index 18623d0ab944f..54edfebbf88a1 100644 --- a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm/BasicAuthSettingsForm.tsx @@ -35,6 +35,7 @@ const schema = object({ DISABLE_SIGNUP: boolean().required(), EXTERNAL_ANONYMOUS_USERS_ENABLED: boolean().required(), SECURITY_MANUAL_LINKING_ENABLED: boolean().required(), + MAILER_AUTOCONFIRM: boolean().required(), SITE_URL: string().required('Must have a Site URL'), }) @@ -52,6 +53,7 @@ const BasicAuthSettingsForm = () => { DISABLE_SIGNUP: true, EXTERNAL_ANONYMOUS_USERS_ENABLED: false, SECURITY_MANUAL_LINKING_ENABLED: false, + MAILER_AUTOCONFIRM: true, SITE_URL: '', }, }) @@ -61,7 +63,8 @@ const BasicAuthSettingsForm = () => { form.reset({ DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP, EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED, - SECURITY_MANUAL_LINKING_ENABLED: authConfig.SECURITY_MANUAL_LINKING_ENABLED || false, + SECURITY_MANUAL_LINKING_ENABLED: authConfig.SECURITY_MANUAL_LINKING_ENABLED, + MAILER_AUTOCONFIRM: authConfig.MAILER_AUTOCONFIRM, SITE_URL: authConfig.SITE_URL, }) } @@ -250,6 +253,27 @@ const BasicAuthSettingsForm = () => { )} + + ( + + + + + + )} + /> + {form.formState.isDirty && ( form.reset()}> diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index c821de8ccfa8c..8044d0c2dc271 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -16,7 +16,7 @@ import { CronJob, useCronJobsQuery } from 'data/database-cron-jobs/database-cron import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Button, Form_Shadcn_, @@ -199,7 +199,7 @@ export const CreateCronJobSheet = ({ onClose, }: CreateCronJobSheetProps) => { const { project } = useProjectContext() - const org = useSelectedOrganization() + const { data: org } = useSelectedOrganizationQuery() const isEditing = !!selectedCronJob?.jobname const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) @@ -209,6 +209,13 @@ export const CreateCronJobSheet = ({ connectionString: project?.connectionString, }) + const { data } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const pgNetExtension = (data ?? []).find((ext) => ext.name === 'pg_net') + const pgNetExtensionInstalled = pgNetExtension?.installed_version != undefined + const { mutate: sendEvent } = useSendEventMutation() const { mutate: upsertCronJob, isLoading } = useDatabaseCronJobCreateMutation() @@ -230,11 +237,8 @@ export const CreateCronJobSheet = ({ }) const isEdited = form.formState.isDirty - // if the form hasn't been touched and the user clicked esc or the backdrop, close the sheet - if (!isEdited && isClosing) { - onClose() - } + if (!isEdited && isClosing) onClose() const onClosePanel = () => { if (isEdited) { @@ -286,6 +290,7 @@ export const CreateCronJobSheet = ({ if (command) { form.setValue('values.snippet', command) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ edgeFunctionName, endpoint, @@ -362,14 +367,6 @@ export const CreateCronJobSheet = ({ ) } - const { data } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const pgNetExtension = (data ?? []).find((ext) => ext.name === 'pg_net') - const pgNetExtensionInstalled = pgNetExtension?.installed_version != undefined - return ( <> diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index fc7c9ffc28907..08827113b6076 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -30,12 +30,20 @@ describe('parseCronJobCommand', () => { }) }) - it('should return a sql function command when the command is SELECT public.test_fn(1, 2)', () => { - const command = 'SELECT public.test_fn(1, 2)' + it('should return a sql function command when the command is SELECT auth.jwt () and ends with ;', () => { + const command = 'SELECT auth.jwt ();' expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ type: 'sql_function', - schema: 'public', - functionName: 'test_fn', + schema: 'auth', + functionName: 'jwt', + snippet: command, + }) + }) + + it('should return a sql snippet command when the command is SELECT public.test_fn(1, 2)', () => { + const command = 'SELECT public.test_fn(1, 2)' + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ + type: 'sql_snippet', snippet: command, }) }) @@ -194,6 +202,14 @@ describe('parseCronJobCommand', () => { }) }) + it('should return SQL snippet type if the command is a HTTP request that cannot be parsed properly due to positional notationa', () => { + const command = `SELECT net.http_post( 'https://webhook.site/dacc2028-a588-462c-9597-c8968e61d0fa', '{"message":"Hello from Supabase"}'::jsonb, '{}'::jsonb, '{"Content-Type":"application/json"}'::jsonb );` + expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({ + type: 'sql_snippet', + snippet: command, + }) + }) + // Array of test cases for secondsPattern const secondsPatternTests = [ { description: '10 seconds', command: '10 seconds' }, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index 08bca3ff2de99..1dbae3bfb44ce 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -73,7 +73,7 @@ export const parseCronJobCommand = (originalCommand: string, projectRef: string) } } else { const headersStringMatch = command.match(/headers:='([^']*)'/i) - const headersString = headersStringMatch?.[1] || '' + const headersString = headersStringMatch?.[1] || '{}' try { const parsedHeaders = JSON.parse(headersString) headersObjs = Object.entries(parsedHeaders).map(([name, value]) => ({ @@ -112,22 +112,25 @@ export const parseCronJobCommand = (originalCommand: string, projectRef: string) } } - return { - type: 'http_request', - method: method === 'http_get' ? 'GET' : 'POST', - endpoint: url, - httpHeaders: headersObjs, - httpBody: body, - timeoutMs: Number(timeout ?? 1000), - snippet: originalCommand, + if (url !== '') { + return { + type: 'http_request', + method: method === 'http_get' ? 'GET' : 'POST', + endpoint: url, + httpHeaders: headersObjs, + httpBody: body, + timeoutMs: Number(timeout ?? 1000), + snippet: originalCommand, + } } } - const regexDBFunction = /select\s+[a-zA-Z-_]*\.?[a-zA-Z-_]*\s*\(.+/g + const regexDBFunction = /select\s+[a-zA-Z-_]*\.?[a-zA-Z-_]*\s*\(\)/g if (command.toLocaleLowerCase().match(regexDBFunction)) { const [schemaName, functionName] = command .replace('SELECT ', '') - .replace(/\(.*\)/, '') + .replace(/\(.*\);*/, '') + .trim() .split('.') diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx index d388404874029..54cd8ce12473a 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx @@ -144,16 +144,16 @@ export const CreateWrapperSheet = ({ } setFormErrors(errors) - if (!isEmpty(errors)) { - return - } + if (!isEmpty(errors)) return try { - await createSchema({ - projectRef: project?.ref, - connectionString: project?.connectionString, - name: values.target_schema, - }) + if (selectedMode === 'schema') { + await createSchema({ + projectRef: project?.ref, + connectionString: project?.connectionString, + name: values.target_schema, + }) + } await createFDW({ projectRef: project?.ref, diff --git a/apps/studio/pages/api/platform/projects/[ref]/analytics/endpoints/[name].ts b/apps/studio/pages/api/platform/projects/[ref]/analytics/endpoints/[name].ts index 1646996989fab..eee1aff5d8eba 100644 --- a/apps/studio/pages/api/platform/projects/[ref]/analytics/endpoints/[name].ts +++ b/apps/studio/pages/api/platform/projects/[ref]/analytics/endpoints/[name].ts @@ -30,14 +30,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } const proxyRequest = async (req: NextApiRequest) => { - const { name, ...toForward } = req.query - const project_tier = 'ENTERPRISE' + const { name, ref: project, ...toForward } = req.query if (req.method === 'GET') { - const payload = { ...toForward, project_tier } + const payload = { ...toForward, project } return retrieveAnalyticsData(name as string, payload) } else if (req.method === 'POST') { - const payload = { ...req.body, project_tier } + const payload = { ...req.body, project } return retrieveAnalyticsData(name as string, payload) } }