From 5bd430ca38fcf07f321e35ef1284ab705c77492d Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 5 Mar 2025 09:42:02 +1000 Subject: [PATCH] Updating authentication mfa and url pages (#33956) * init PrePostTab component * init * support auto fixing rounded of inputs/select/textarea * remove form item layout prepost * Update FormLayout.tsx * Update FormLayout.tsx * Update FormLayout.tsx * form layout and advanced * Update advanced.tsx * protection and hooks * mfa and url * remove onchange --------- Co-authored-by: Jonathan Summers-Muir --- .../MfaAuthSettingsForm.tsx | 511 ++++++++++++------ .../Auth/RedirectUrls/RedirectUrls.tsx | 25 +- .../interfaces/Auth/SiteUrl/SiteUrl.tsx | 153 +++--- apps/studio/pages/project/[ref]/auth/mfa.tsx | 70 ++- .../project/[ref]/auth/url-configuration.tsx | 79 ++- 5 files changed, 526 insertions(+), 312 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx index d2233eb9054..df39466a5de 100644 --- a/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/MfaAuthSettingsForm/MfaAuthSettingsForm.tsx @@ -1,13 +1,12 @@ +import { yupResolver } from '@hookform/resolvers/yup' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { number, object, string } from 'yup' import { useParams } from 'common' -import { FormActions } from 'components/ui/Forms/FormActions' -import { FormHeader } from 'components/ui/Forms/FormHeader' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useAuthConfigQuery } from 'data/auth/auth-config-query' @@ -16,28 +15,27 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' -import FormField from '../AuthProvidersForm/FormField' - import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, - Form, - InputNumber, + Button, + Card, + CardContent, + CardFooter, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Input_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, WarningIcon, + PrePostTab, } from 'ui' - -const schema = object({ - MFA_PHONE_OTP_LENGTH: number() - .min(6, 'Must be a value 6 or larger') - .max(30, 'must be a value no greater than 30'), - MFA_PHONE_TEMPLATE: string().required('SMS template is required.'), - MFA_MAX_ENROLLED_FACTORS: number() - .min(0, 'Must be a value 0 or larger') - .max(30, 'Must be a value no greater than 30'), - MFA_TOTP: string().required(), - MFA_PHONE: string().required(), -}) +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' function determineMFAStatus(verifyEnabled: boolean, enrollEnabled: boolean) { return verifyEnabled ? (enrollEnabled ? 'Enabled' : 'Verify Enabled') : 'Disabled' @@ -66,6 +64,21 @@ const MfaStatusToState = (status: (typeof MFAFactorSelectionOptions)[number]['va : { verifyEnabled: false, enrollEnabled: false } } +const totpSchema = object({ + MFA_TOTP: string().required(), + MFA_MAX_ENROLLED_FACTORS: number() + .min(0, 'Must be a value 0 or larger') + .max(30, 'Must be a value no greater than 30'), +}) + +const phoneSchema = object({ + MFA_PHONE: string().required(), + MFA_PHONE_OTP_LENGTH: number() + .min(6, 'Must be a value 6 or larger') + .max(30, 'must be a value no greater than 30'), + MFA_PHONE_TEMPLATE: string().required('SMS template is required.'), +}) + const MfaAuthSettingsForm = () => { const { ref: projectRef } = useParams() const { @@ -73,11 +86,13 @@ const MfaAuthSettingsForm = () => { error: authConfigError, isLoading, isError, - isSuccess, } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() + + // Separate loading states for each form + const [isUpdatingTotpForm, setIsUpdatingTotpForm] = useState(false) + const [isUpdatingPhoneForm, setIsUpdatingPhoneForm] = useState(false) - const formId = 'auth-config-advanced-form' const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') @@ -94,34 +109,87 @@ const MfaAuthSettingsForm = () => { (addon) => addon.type === 'auth_mfa_phone' ) - const INITIAL_VALUES = { - MFA_PHONE_OTP_LENGTH: authConfig?.MFA_PHONE_OTP_LENGTH || 6, - MFA_PHONE_TEMPLATE: authConfig?.MFA_PHONE_TEMPLATE || 'Your code is {{ .Code }}', - MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS ?? 10, - MFA_TOTP: - determineMFAStatus( - authConfig?.MFA_TOTP_VERIFY_ENABLED ?? true, - authConfig?.MFA_TOTP_ENROLL_ENABLED ?? true - ) || 'Enabled', - MFA_PHONE: - determineMFAStatus( - authConfig?.MFA_PHONE_VERIFY_ENABLED || false, - authConfig?.MFA_PHONE_ENROLL_ENABLED || false - ) || 'Disabled', - } - // For now, we support Twilio and Vonage. Twilio Verify is not supported and the remaining providers are community maintained. const sendSMSHookIsEnabled = authConfig?.HOOK_SEND_SMS_URI !== null && authConfig?.HOOK_SEND_SMS_ENABLED === true const hasValidMFAPhoneProvider = authConfig?.EXTERNAL_PHONE_ENABLED === true const hasValidMFAProvider = hasValidMFAPhoneProvider || sendSMSHookIsEnabled - const onSubmit = (values: any, { resetForm }: any) => { - let payload = { ...values } + const totpForm = useForm({ + resolver: yupResolver(totpSchema), + defaultValues: { + MFA_TOTP: 'Enabled', + MFA_MAX_ENROLLED_FACTORS: 10, + }, + }) + + const phoneForm = useForm({ + resolver: yupResolver(phoneSchema), + defaultValues: { + MFA_PHONE: 'Disabled', + MFA_PHONE_OTP_LENGTH: 6, + MFA_PHONE_TEMPLATE: 'Your code is {{ .Code }}', + }, + }) + + useEffect(() => { + if (authConfig) { + if (!isUpdatingTotpForm) { + totpForm.reset({ + MFA_TOTP: + determineMFAStatus( + authConfig?.MFA_TOTP_VERIFY_ENABLED ?? true, + authConfig?.MFA_TOTP_ENROLL_ENABLED ?? true + ) || 'Enabled', + MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS ?? 10, + }) + } + + if (!isUpdatingPhoneForm) { + phoneForm.reset({ + MFA_PHONE: + determineMFAStatus( + authConfig?.MFA_PHONE_VERIFY_ENABLED || false, + authConfig?.MFA_PHONE_ENROLL_ENABLED || false + ) || 'Disabled', + MFA_PHONE_OTP_LENGTH: authConfig?.MFA_PHONE_OTP_LENGTH || 6, + MFA_PHONE_TEMPLATE: authConfig?.MFA_PHONE_TEMPLATE || 'Your code is {{ .Code }}', + }) + } + } + }, [authConfig, isUpdatingTotpForm, isUpdatingPhoneForm]) + + const onSubmitTotpForm = (values: any) => { const { verifyEnabled: MFA_TOTP_VERIFY_ENABLED, enrollEnabled: MFA_TOTP_ENROLL_ENABLED } = MfaStatusToState(values.MFA_TOTP) - // MFA (Phone) is only available on Pro Plans and up. We translate the UI state, MFA_PHONE and MFA_TOTP into the underlying - // Auth config state - MFA_PHONE_*_ENABLED and MFA_TOTP_*_ENABLED. + + const payload = { + ...values, + MFA_TOTP_ENROLL_ENABLED, + MFA_TOTP_VERIFY_ENABLED, + } + delete payload.MFA_TOTP + + setIsUpdatingTotpForm(true) + + updateAuthConfig( + { projectRef: projectRef!, config: payload }, + { + onError: (error) => { + toast.error(`Failed to update TOTP settings: ${error?.message}`) + setIsUpdatingTotpForm(false) + }, + onSuccess: () => { + toast.success('Successfully updated TOTP settings') + setIsUpdatingTotpForm(false) + }, + } + ) + } + + const onSubmitPhoneForm = (values: any) => { + let payload = { ...values } + if (isProPlanAndUp) { const { verifyEnabled: MFA_PHONE_VERIFY_ENABLED, enrollEnabled: MFA_PHONE_ENROLL_ENABLED } = MfaStatusToState(values.MFA_PHONE) @@ -131,23 +199,20 @@ const MfaAuthSettingsForm = () => { MFA_PHONE_VERIFY_ENABLED, } } - payload = { - ...payload, - MFA_TOTP_ENROLL_ENABLED, - MFA_TOTP_VERIFY_ENABLED, - } - delete payload.MFA_TOTP delete payload.MFA_PHONE + setIsUpdatingPhoneForm(true) + updateAuthConfig( { projectRef: projectRef!, config: payload }, { onError: (error) => { - toast.error(`Failed to update settings: ${error?.message}`) + toast.error(`Failed to update phone MFA settings: ${error?.message}`) + setIsUpdatingPhoneForm(false) }, onSuccess: () => { - toast.success('Successfully updated settings') - resetForm({ values: values, initialValues: values }) + toast.success('Successfully updated phone MFA settings') + setIsUpdatingPhoneForm(false) }, } ) @@ -167,134 +232,240 @@ const MfaAuthSettingsForm = () => { return } + const phoneMFAIsEnabled = + phoneForm.watch('MFA_PHONE') === 'Enabled' || phoneForm.watch('MFA_PHONE') === 'Verify Enabled' + const hasUpgradedPhoneMFA = authConfig?.MFA_PHONE_VERIFY_ENABLED === false && phoneMFAIsEnabled + return ( -
- {({ handleReset, resetForm, values, initialValues, setFieldValue }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + <> + + + Multi-Factor Authentication (MFA) + - // Form is reset once remote data is loaded in store - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (isSuccess) resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) - }, [isSuccess]) + + + + + ( + + + + + + + + {MFAFactorSelectionOptions.map((option) => ( + + {option.label} + + ))} + + + + + )} + /> + - const hasUpgradedPhoneMFA = - INITIAL_VALUES.MFA_PHONE === 'Disabled' && values.MFA_PHONE !== INITIAL_VALUES.MFA_PHONE - const phoneMFAIsEnabled = - values.MFA_PHONE === 'Enabled' || values.MFA_PHONE === 'Verify Enabled' + + ( + + + + + + + + )} + /> + - return ( - <> - - - - } - > - Multi-Factor Authentication (MFA)} - > - - + + {totpForm.formState.isDirty && ( + + )} + + + + + +
- factors} - disabled={!canUpdateConfig} - /> - - - SMS MFA}> - - {promptProPlanUpgrade && ( + + SMS MFA + + +
+ + + {promptProPlanUpgrade && ( +
- )} - - {!hasValidMFAProvider && phoneMFAIsEnabled && ( - - - - To use MFA with Phone you should set up a Phone provider or Send SMS Hook. - - - )} +
+ )} - - - {hasUpgradedPhoneMFA && ( - - - - Enabling advanced MFA with phone will result in an additional charge of $75 - per month for the first project in the organization and an additional $10 - per month for additional projects. - - + ( + + + + + + + + {MFAFactorSelectionOptions.map((option) => ( + + {option.label} + + ))} + + + + )} - - - - - ) - }} - + /> + + {!hasValidMFAProvider && phoneMFAIsEnabled && ( + + + + To use MFA with Phone you should set up a Phone provider or Send SMS Hook. + + + )} +
+ + + ( + + + + + + )} + /> + + + + ( + + + + + + )} + /> + + + {hasUpgradedPhoneMFA && ( + + + + + Enabling advanced MFA with phone will result in an additional charge of $75 + per month for the first project in the organization and an additional $10 per + month for additional projects. + + + + )} + + + {phoneForm.formState.isDirty && ( + + )} + + +
+ +
+
+ ) } diff --git a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx index 8202b5e94cb..32094c4986b 100644 --- a/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx +++ b/apps/studio/components/interfaces/Auth/RedirectUrls/RedirectUrls.tsx @@ -2,8 +2,12 @@ import { useMemo, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' +import { + ScaffoldDescription, + ScaffoldSection, + ScaffoldSectionTitle, +} from 'components/layouts/Scaffold' import { DocsButton } from 'components/ui/DocsButton' -import { FormHeader } from 'components/ui/Forms/FormHeader' import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers/Shimmers' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' @@ -69,15 +73,18 @@ export const RedirectUrls = () => { } return ( -
+
- +
+ Redirect URLs + + URLs that auth providers are permitted to redirect to post authentication. Wildcards are + allowed, for example, https://*.domain.com + +
+ {isLoading && ( <> @@ -88,6 +95,7 @@ export const RedirectUrls = () => { )} + {isError && ( @@ -95,6 +103,7 @@ export const RedirectUrls = () => { {authConfigError.message} )} + {isSuccess && ( { -
+ ) } diff --git a/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx b/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx index 133b56454a1..e9e785b9c19 100644 --- a/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx +++ b/apps/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx @@ -1,17 +1,29 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { object, string } from 'yup' +import { yupResolver } from '@hookform/resolvers/yup' import { useParams } from 'common' -import { FormActions } from 'components/ui/Forms/FormActions' -import { FormHeader } from 'components/ui/Forms/FormHeader' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection' +import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Form, Input } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Card, + CardContent, + CardFooter, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Input_Shadcn_, +} from 'ui' import { AlertCircle } from 'lucide-react' const schema = object({ @@ -20,31 +32,40 @@ const schema = object({ const SiteUrl = () => { const { ref: projectRef } = useParams() - const { - data: authConfig, - error: authConfigError, - isLoading, - isError, - isSuccess, - } = useAuthConfigQuery({ projectRef }) - const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation() + const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef }) + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation() + const [isUpdatingSiteUrl, setIsUpdatingSiteUrl] = useState(false) - const formId = 'auth-config-general-form' const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') - const INITIAL_VALUES = { SITE_URL: authConfig?.SITE_URL } + const siteUrlForm = useForm({ + resolver: yupResolver(schema), + defaultValues: { + SITE_URL: '', + }, + }) + + useEffect(() => { + if (authConfig && !isUpdatingSiteUrl) { + siteUrlForm.reset({ + SITE_URL: authConfig.SITE_URL || '', + }) + } + }, [authConfig, isUpdatingSiteUrl]) + + const onSubmitSiteUrl = (values: any) => { + setIsUpdatingSiteUrl(true) - const onSubmit = (values: any, { resetForm }: any) => { - const payload = { ...values } updateAuthConfig( - { projectRef: projectRef!, config: payload }, + { projectRef: projectRef!, config: values }, { - onError: () => { - toast.error('Failed to update settings') + onError: (error) => { + toast.error(`Failed to update site URL: ${error?.message}`) + setIsUpdatingSiteUrl(false) }, onSuccess: () => { - toast.success('Successfully updated settings') - resetForm({ values: values, initialValues: values }) + toast.success('Successfully updated site URL') + setIsUpdatingSiteUrl(false) }, } ) @@ -61,53 +82,49 @@ const SiteUrl = () => { } return ( -
- {({ handleReset, resetForm, values, initialValues }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) + + Site URL - // Form is reset once remote data is loaded in store - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (isSuccess) { - resetForm({ values: INITIAL_VALUES, initialValues: INITIAL_VALUES }) - } - }, [isSuccess]) + + + + + ( + + + + + + )} + /> + - return ( - <> - - - - - } - > - - - - - - - - ) - }} - + + {siteUrlForm.formState.isDirty && ( + + )} + + + + + +
) } diff --git a/apps/studio/pages/project/[ref]/auth/mfa.tsx b/apps/studio/pages/project/[ref]/auth/mfa.tsx index 8bdcd91c193..6d16f76bcb1 100644 --- a/apps/studio/pages/project/[ref]/auth/mfa.tsx +++ b/apps/studio/pages/project/[ref]/auth/mfa.tsx @@ -1,54 +1,50 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ReactNode } from 'react' import { MfaAuthSettingsForm } from 'components/interfaces/Auth' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' -import { - ScaffoldContainer, - ScaffoldDescription, - ScaffoldHeader, - ScaffoldTitle, -} from 'components/layouts/Scaffold' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -const PageLayout: NextPageWithLayout = () => { - const isPermissionsLoaded = usePermissionsLoaded() - // TODO: check if these permissions cover third party auth as well - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - - return ( - <> - - - Multi-Factor Authentication (MFA) - - Requires users to provide additional verification factors to authenticate - - - - - {!isPermissionsLoaded ? ( - - ) : !canReadAuthSettings ? ( - - ) : ( - <> - - - )} - - - ) +interface MfaLayoutProps { + children: ReactNode } -PageLayout.getLayout = (page) => { +export const MfaLayout = ({ children }: MfaLayoutProps) => { return ( - {page} + + + {children} + + ) } -export default PageLayout + +const MfaPage: NextPageWithLayout = () => { + const isPermissionsLoaded = usePermissionsLoaded() + const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') + + if (isPermissionsLoaded && !canReadAuthSettings) { + return + } + + return ( + + {!isPermissionsLoaded ? : } + + ) +} + +MfaPage.getLayout = (page) => {page} + +export default MfaPage diff --git a/apps/studio/pages/project/[ref]/auth/url-configuration.tsx b/apps/studio/pages/project/[ref]/auth/url-configuration.tsx index 0eb6ebb8f4b..2650af0c08a 100644 --- a/apps/studio/pages/project/[ref]/auth/url-configuration.tsx +++ b/apps/studio/pages/project/[ref]/auth/url-configuration.tsx @@ -1,42 +1,63 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ReactNode } from 'react' +import { useParams } from 'common' import { RedirectUrls } from 'components/interfaces/Auth/RedirectUrls/RedirectUrls' import SiteUrl from 'components/interfaces/Auth/SiteUrl/SiteUrl' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' -import { ScaffoldContainer, ScaffoldHeader, ScaffoldTitle } from 'components/layouts/Scaffold' -import { FormsContainer } from 'components/ui/Forms/FormsContainer' +import DefaultLayout from 'components/layouts/DefaultLayout' +import { PageLayout } from 'components/layouts/PageLayout/PageLayout' +import { ScaffoldContainer } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -import DefaultLayout from 'components/layouts/DefaultLayout' -const URLConfiguration: NextPageWithLayout = () => { - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') - const isPermissionsLoaded = usePermissionsLoaded() - if (isPermissionsLoaded && !canReadAuthSettings) { - return - } else { - return ( -
- - - URL Configuration - - - - - - - -
- ) - } +interface URLConfigurationLayoutProps { + children: ReactNode } -URLConfiguration.getLayout = (page) => ( - - {page} - -) +export const URLConfigurationLayout = ({ children }: URLConfigurationLayoutProps) => { + const { ref } = useParams() + + return ( + + + + {children} + + + + ) +} + +const URLConfiguration: NextPageWithLayout = () => { + const isPermissionsLoaded = usePermissionsLoaded() + const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') + + if (isPermissionsLoaded && !canReadAuthSettings) { + return + } + + return ( + + {!isPermissionsLoaded ? ( +
+ +
+ ) : ( + <> + + + + )} +
+ ) +} + +URLConfiguration.getLayout = (page) => {page} export default URLConfiguration