Update authentication advanced page (#33955)

* 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

* fix

* parse

---------

Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
Saxon FletcherandJonathan Summers-Muir authored and Joshen Lim committed 2025-03-05 15:39:02 +01:00
1 parent 9939cfc670
commit 3718608cf0
10 files changed
+513 -312

No files matched your search

@@ -1,15 +1,12 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useEffect, useMemo } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
import { useParams } from 'common'
import { StringNumberOrNull } from 'components/ui/Forms/Form.constants'
import { FormActions } from 'components/ui/Forms/FormActions'
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'
@@ -22,15 +19,19 @@ import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Card,
CardContent,
CardFooter,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
PrePostTab,
WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const formId = 'auth-config-advanced-form'
import { StringNumberOrNull } from 'components/ui/Forms/Form.constants'
const FormSchema = z.object({
API_MAX_REQUEST_DURATION: z.coerce
@@ -46,69 +47,110 @@ export const AdvancedAuthSettingsForm = () => {
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const [isUpdatingRequestDurationForm, setIsUpdatingRequestDurationForm] = useState(false)
const [isUpdatingDatabaseForm, setIsUpdatingDatabaseForm] = useState(false)
const {
data: authConfig,
error: authConfigError,
isLoading,
isError,
isSuccess,
} = useAuthConfigQuery({ projectRef })
const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({
orgSlug: organization?.slug,
})
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
const defaultValues = useMemo(
() => ({
API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION ?? undefined,
DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE ?? '',
}),
[authConfig]
)
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: defaultValues as any,
})
const { isDirty } = form.formState
const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
const isTeamsEnterprisePlan =
isSuccessSubscription && subscription.plan.id !== 'free' && subscription.plan.id !== 'pro'
isSuccessSubscription && subscription?.plan.id !== 'free' && subscription?.plan.id !== 'pro'
const promptTeamsEnterpriseUpgrade =
IS_PLATFORM && isSuccessSubscription && !isTeamsEnterprisePlan
const resetForm = () => {
form.reset(defaultValues as any)
}
const requestDurationForm = useForm({
resolver: zodResolver(
z.object({
API_MAX_REQUEST_DURATION: FormSchema.shape.API_MAX_REQUEST_DURATION,
})
),
defaultValues: {
API_MAX_REQUEST_DURATION: 10,
},
})
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = (data) => {
if (!projectRef) return console.error('Project ref is required')
const databaseForm = useForm({
resolver: zodResolver(
z.object({
DB_MAX_POOL_SIZE: FormSchema.shape.DB_MAX_POOL_SIZE,
})
),
defaultValues: {
DB_MAX_POOL_SIZE: '',
},
})
const { API_MAX_REQUEST_DURATION, DB_MAX_POOL_SIZE, ...config } = data
const payload = {
...config,
...(isTeamsEnterprisePlan ? { API_MAX_REQUEST_DURATION } : {}),
DB_MAX_POOL_SIZE,
useEffect(() => {
if (authConfig) {
if (!isUpdatingRequestDurationForm) {
requestDurationForm.reset({
API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION ?? 10,
})
}
if (!isUpdatingDatabaseForm) {
databaseForm.reset({
DB_MAX_POOL_SIZE:
authConfig?.DB_MAX_POOL_SIZE !== null ? String(authConfig?.DB_MAX_POOL_SIZE) : '',
})
}
}
}, [authConfig, isUpdatingRequestDurationForm, isUpdatingDatabaseForm])
const onSubmitRequestDurationForm = (values: any) => {
if (!projectRef) return console.error('Project ref is required')
if (!isTeamsEnterprisePlan) return
setIsUpdatingRequestDurationForm(true)
updateAuthConfig(
// @ts-expect-error
{ projectRef: projectRef, config: payload },
{ projectRef: projectRef, config: values },
{
onError: (error) => {
toast.error(`Failed to update request duration settings: ${error?.message}`)
setIsUpdatingRequestDurationForm(false)
},
onSuccess: () => {
toast.success('Successfully updated settings')
form.reset({ ...defaultValues, ...data })
toast.success('Successfully updated request duration settings')
setIsUpdatingRequestDurationForm(false)
},
}
)
}
useEffect(() => {
// reset form with the current values
if (isSuccess) resetForm()
}, [defaultValues, isSuccess])
const onSubmitDatabaseForm = (values: any) => {
if (!projectRef) return console.error('Project ref is required')
setIsUpdatingDatabaseForm(true)
const config = {
DB_MAX_POOL_SIZE: values.DB_MAX_POOL_SIZE,
}
updateAuthConfig(
{ projectRef: projectRef, config },
{
onError: (error) => {
toast.error(`Failed to update database connection settings: ${error?.message}`)
setIsUpdatingDatabaseForm(false)
},
onSuccess: () => {
toast.success('Successfully updated database connection settings')
setIsUpdatingDatabaseForm(false)
},
}
)
}
if (isError) {
return (
@@ -127,88 +169,126 @@ export const AdvancedAuthSettingsForm = () => {
return (
<>
{promptTeamsEnterpriseUpgrade && (
<UpgradeToPro
primaryText="Upgrade to Team or Enterprise"
secondaryText="Advanced Auth server settings are only available on the Team Plan and up."
buttonText="Upgrade to Team"
/>
<div className="my-4">
<UpgradeToPro
primaryText="Upgrade to Team or Enterprise"
secondaryText="Advanced Auth server settings are only available on the Team Plan and up."
buttonText="Upgrade to Team"
/>
</div>
)}
<Form_Shadcn_ {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
<FormPanel
footer={
<div className="flex py-4 px-8">
<FormActions
form={formId}
isSubmitting={isUpdatingConfig}
hasChanges={isDirty}
handleReset={() => resetForm()}
disabled={!canUpdateConfig}
helper={
!canUpdateConfig
? 'You need additional permissions to update authentication settings'
: undefined
}
/>
</div>
}
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">Request Duration</ScaffoldSectionTitle>
<Form_Shadcn_ {...requestDurationForm}>
<form
onSubmit={requestDurationForm.handleSubmit(onSubmitRequestDurationForm)}
className="space-y-4"
>
<FormSection header={<FormSectionLabel>Max Request Duration</FormSectionLabel>}>
<FormSectionContent loading={isLoading}>
<Card>
<CardContent className="pt-6">
<FormField_Shadcn_
control={form.control}
control={requestDurationForm.control}
name="API_MAX_REQUEST_DURATION"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Maximum time allowed for an Auth request to last"
description="Number of seconds to wait for an Auth request to complete before canceling it. In certain high-load situations setting a larger or smaller value can be used to control load-shedding. Recommended: 10 seconds."
>
<FormControl_Shadcn_>
<div className="relative">
<Input_Shadcn_
type="number"
disabled={!canUpdateConfig || promptTeamsEnterpriseUpgrade}
placeholder="10"
{...field}
/>
<span className="text-sm text-foreground-lighter absolute top-1.5 right-3">
seconds
</span>
<PrePostTab postTab="seconds">
<Input_Shadcn_
type="number"
min={5}
max={30}
{...field}
disabled={!canUpdateConfig || promptTeamsEnterpriseUpgrade}
/>
</PrePostTab>
</div>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</FormSectionContent>
</FormSection>
</CardContent>
<FormSection header={<FormSectionLabel>Auth Database Connections</FormSectionLabel>}>
<FormSectionContent loading={isLoading}>
<CardFooter className="justify-end space-x-2">
{requestDurationForm.formState.isDirty && (
<Button type="default" onClick={() => requestDurationForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={
!canUpdateConfig ||
isUpdatingRequestDurationForm ||
!requestDurationForm.formState.isDirty ||
promptTeamsEnterpriseUpgrade
}
loading={isUpdatingRequestDurationForm}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
</ScaffoldSection>
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">Auth Database Connections</ScaffoldSectionTitle>
<Form_Shadcn_ {...databaseForm}>
<form onSubmit={databaseForm.handleSubmit(onSubmitDatabaseForm)} className="space-y-4">
<Card>
<CardContent className="pt-6">
<FormField_Shadcn_
control={form.control}
control={databaseForm.control}
name="DB_MAX_POOL_SIZE"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Max Direct Auth Connections"
description="Auth will take up no more than this number of connections from the total number of available connections to serve requests. These connections are not reserved, so when unused they are released. Defaults to 10 connections."
>
<FormControl_Shadcn_>
<Input_Shadcn_
type="number"
disabled={!canUpdateConfig || promptTeamsEnterpriseUpgrade}
placeholder="10"
{...field}
value={field.value || ''}
disabled={!canUpdateConfig || promptTeamsEnterpriseUpgrade}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</FormSectionContent>
</FormSection>
</FormPanel>
</form>
</Form_Shadcn_>
</CardContent>
<CardFooter className="justify-end space-x-2">
{databaseForm.formState.isDirty && (
<Button type="default" onClick={() => databaseForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={
!canUpdateConfig || isUpdatingDatabaseForm || !databaseForm.formState.isDirty
}
loading={isUpdatingDatabaseForm}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
</ScaffoldSection>
</>
)
}
@@ -16,6 +16,7 @@ import { CreateHookSheet } from './CreateHookSheet'
import { HookCard } from './HookCard'
import { HOOKS_DEFINITIONS, HOOK_DEFINITION_TITLE, Hook } from './hooks.constants'
import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils'
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
export const HooksListing = () => {
const { ref: projectRef } = useParams()
@@ -68,8 +69,11 @@ export const HooksListing = () => {
}
return (
<div className="pb-4">
<FormHeader title="All hooks" actions={<AddHookDropdown onSelectHook={setSelectedHook} />} />
<ScaffoldSection isFullWidth>
<div className="flex justify-between items-center mb-4">
<ScaffoldSectionTitle>All hooks</ScaffoldSectionTitle>
<AddHookDropdown onSelectHook={setSelectedHook} />
</div>
{hooks.filter((h) => isValidHook(h)).length === 0 && (
<div
@@ -155,6 +159,6 @@ export const HooksListing = () => {
)}
</div>
</ConfirmationModal>
</div>
</ScaffoldSection>
)
}
@@ -1,30 +1,40 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { boolean, number, 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, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
import NoPermission from 'components/ui/NoPermission'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Form,
Input,
Toggle,
Card,
CardContent,
CardFooter,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Switch,
WarningIcon,
PrePostTab,
} from 'ui'
import { NO_REQUIRED_CHARACTERS } from '../Auth.constants'
import FormField from '../AuthProvidersForm/FormField'
const schema = object({
DISABLE_SIGNUP: boolean().required(),
@@ -51,8 +61,6 @@ const schema = object({
PASSWORD_HIBP_ENABLED: boolean(),
})
const formId = 'auth-config-protection-settings'
const ProtectionAuthSettingsForm = () => {
const { ref: projectRef } = useParams()
const {
@@ -60,32 +68,57 @@ const ProtectionAuthSettingsForm = () => {
error: authConfigError,
isLoading,
isError,
isSuccess,
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
const [isUpdatingProtection, setIsUpdatingProtection] = useState(false)
const [hidden, setHidden] = useState(true)
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const INITIAL_VALUES = {
DISABLE_SIGNUP: !authConfig?.DISABLE_SIGNUP,
EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED,
SECURITY_MANUAL_LINKING_ENABLED: authConfig?.SECURITY_MANUAL_LINKING_ENABLED || false,
SITE_URL: authConfig?.SITE_URL,
SECURITY_CAPTCHA_ENABLED: authConfig?.SECURITY_CAPTCHA_ENABLED || false,
SECURITY_CAPTCHA_SECRET: authConfig?.SECURITY_CAPTCHA_SECRET || '',
SECURITY_CAPTCHA_PROVIDER: authConfig?.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha',
SESSIONS_TIMEBOX: authConfig?.SESSIONS_TIMEBOX || 0,
SESSIONS_INACTIVITY_TIMEOUT: authConfig?.SESSIONS_INACTIVITY_TIMEOUT || 0,
SESSIONS_SINGLE_PER_USER: authConfig?.SESSIONS_SINGLE_PER_USER || false,
PASSWORD_MIN_LENGTH: authConfig?.PASSWORD_MIN_LENGTH || 6,
PASSWORD_REQUIRED_CHARACTERS:
authConfig?.PASSWORD_REQUIRED_CHARACTERS || NO_REQUIRED_CHARACTERS,
PASSWORD_HIBP_ENABLED: authConfig?.PASSWORD_HIBP_ENABLED || false,
}
const protectionForm = useForm({
resolver: yupResolver(schema),
defaultValues: {
DISABLE_SIGNUP: true,
EXTERNAL_ANONYMOUS_USERS_ENABLED: false,
SECURITY_MANUAL_LINKING_ENABLED: false,
SITE_URL: '',
SECURITY_CAPTCHA_ENABLED: false,
SECURITY_CAPTCHA_SECRET: '',
SECURITY_CAPTCHA_PROVIDER: 'hcaptcha',
SESSIONS_TIMEBOX: 0,
SESSIONS_INACTIVITY_TIMEOUT: 0,
SESSIONS_SINGLE_PER_USER: false,
PASSWORD_MIN_LENGTH: 6,
PASSWORD_REQUIRED_CHARACTERS: NO_REQUIRED_CHARACTERS,
PASSWORD_HIBP_ENABLED: false,
},
})
useEffect(() => {
if (authConfig && !isUpdatingProtection) {
protectionForm.reset({
DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP,
EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED || false,
SECURITY_MANUAL_LINKING_ENABLED: authConfig.SECURITY_MANUAL_LINKING_ENABLED || false,
SITE_URL: authConfig.SITE_URL || '',
SECURITY_CAPTCHA_ENABLED: authConfig.SECURITY_CAPTCHA_ENABLED || false,
SECURITY_CAPTCHA_SECRET: authConfig.SECURITY_CAPTCHA_SECRET || '',
SECURITY_CAPTCHA_PROVIDER: authConfig.SECURITY_CAPTCHA_PROVIDER || 'hcaptcha',
SESSIONS_TIMEBOX: authConfig.SESSIONS_TIMEBOX || 0,
SESSIONS_INACTIVITY_TIMEOUT: authConfig.SESSIONS_INACTIVITY_TIMEOUT || 0,
SESSIONS_SINGLE_PER_USER: authConfig.SESSIONS_SINGLE_PER_USER || false,
PASSWORD_MIN_LENGTH: authConfig.PASSWORD_MIN_LENGTH || 6,
PASSWORD_REQUIRED_CHARACTERS:
authConfig.PASSWORD_REQUIRED_CHARACTERS || NO_REQUIRED_CHARACTERS,
PASSWORD_HIBP_ENABLED: authConfig.PASSWORD_HIBP_ENABLED || false,
})
}
}, [authConfig, isUpdatingProtection])
const onSubmitProtection = (values: any) => {
setIsUpdatingProtection(true)
const onSubmit = (values: any, { resetForm }: any) => {
const payload = { ...values }
payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP
// The backend uses empty string to represent no required characters in the password
@@ -97,11 +130,12 @@ const ProtectionAuthSettingsForm = () => {
{ projectRef: projectRef!, config: payload },
{
onError: (error) => {
toast.error(`Failed to update settings: ${error?.message}`)
toast.error(`Failed to update settings: ${error?.message}`)
setIsUpdatingProtection(false)
},
onSuccess: () => {
toast.success(`Successfully updated settings`)
resetForm({ values: values, initialValues: values })
toast.success('Successfully updated settings')
setIsUpdatingProtection(false)
},
}
)
@@ -122,106 +156,122 @@ const ProtectionAuthSettingsForm = () => {
}
return (
<div>
<FormHeader
title="Bot and Abuse Protection"
description="Configure protection settings for your application."
/>
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">Bot and Abuse Protection</ScaffoldSectionTitle>
<Form
id={formId}
initialValues={INITIAL_VALUES}
onSubmit={onSubmit}
validationSchema={schema}
>
{({ handleReset, resetForm, values, initialValues, setFieldValue }: any) => {
const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
// 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])
<Form_Shadcn_ {...protectionForm}>
<form onSubmit={protectionForm.handleSubmit(onSubmitProtection)} className="space-y-4">
<Card>
<CardContent>
<FormField_Shadcn_
control={protectionForm.control}
name="SECURITY_CAPTCHA_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enable Captcha protection"
description="Protect authentication endpoints from bots and abuse."
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
return (
<>
<FormPanel
disabled={true}
footer={
<div className="flex py-4 px-8">
<FormActions
form={formId}
isSubmitting={isUpdatingConfig}
hasChanges={hasChanges}
handleReset={handleReset}
disabled={!canUpdateConfig}
helper={
!canUpdateConfig
? 'You need additional permissions to update authentication settings'
: undefined
}
/>
</div>
}
>
<FormSection header={<FormSectionLabel>Bot and Abuse Protection</FormSectionLabel>}>
<FormSectionContent loading={isLoading}>
<Toggle
id="SECURITY_CAPTCHA_ENABLED"
size="small"
label="Enable Captcha protection"
layout="flex"
descriptionText="Protect authentication endpoints from bots and abuse."
disabled={!canUpdateConfig}
/>
{values.SECURITY_CAPTCHA_ENABLED && (
<>
<FormField
name="SECURITY_CAPTCHA_PROVIDER"
properties={{
type: 'select',
title: 'Choose Captcha Provider',
description: '',
enum: [
{
label: 'hCaptcha',
value: 'hcaptcha',
icon: 'hcaptcha-icon.png',
},
{
label: 'Turnstile by Cloudflare',
value: 'turnstile',
icon: 'cloudflare-icon.png',
},
],
}}
formValues={values}
setFieldValue={setFieldValue}
/>
<Input
id="SECURITY_CAPTCHA_SECRET"
type={hidden ? 'password' : 'text'}
size="small"
label="Captcha secret"
descriptionText="Obtain this secret from the provider."
disabled={!canUpdateConfig}
actions={
<Button
icon={hidden ? <Eye /> : <EyeOff />}
type="default"
onClick={() => setHidden(!hidden)}
/>
}
/>
</>
{protectionForm.watch('SECURITY_CAPTCHA_ENABLED') && (
<>
<CardContent>
<FormField_Shadcn_
control={protectionForm.control}
name="SECURITY_CAPTCHA_PROVIDER"
render={({ field }) => (
<FormItemLayout layout="flex-row-reverse" label="Choose Captcha Provider">
<FormControl_Shadcn_>
<Select_Shadcn_
value={field.value}
onValueChange={field.onChange}
disabled={!canUpdateConfig}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select provider" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="hcaptcha">hCaptcha</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="turnstile">
Turnstile by Cloudflare
</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormControl_Shadcn_>
</FormItemLayout>
)}
</FormSectionContent>
</FormSection>
</FormPanel>
</>
)
}}
</Form>
</div>
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={protectionForm.control}
name="SECURITY_CAPTCHA_SECRET"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Captcha secret"
description="Obtain this secret from the provider."
>
<FormControl_Shadcn_>
<div className="flex items-center gap-2">
<PrePostTab
postTab={
<Button
type="text"
className="p-0"
onClick={() => setHidden(!hidden)}
icon={hidden ? <Eye /> : <EyeOff />}
/>
}
>
<Input_Shadcn_
{...field}
type={hidden ? 'password' : 'text'}
disabled={!canUpdateConfig}
/>
</PrePostTab>
</div>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
</>
)}
<CardFooter className="justify-end space-x-2">
{protectionForm.formState.isDirty && (
<Button type="default" onClick={() => protectionForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={
!canUpdateConfig || isUpdatingProtection || !protectionForm.formState.isDirty
}
loading={isUpdatingProtection}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
</ScaffoldSection>
)
}
@@ -1,44 +1,56 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ReactNode } from 'react'
import { useParams } from 'common'
import { AdvancedAuthSettingsForm } from 'components/interfaces/Auth/AdvancedAuthSettingsForm'
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer, 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 (
<>
<ScaffoldContainer>
<ScaffoldHeader>
<ScaffoldTitle>Advanced</ScaffoldTitle>
</ScaffoldHeader>
</ScaffoldContainer>
<ScaffoldContainer className="flex flex-col gap-10" bottomPadding>
{!isPermissionsLoaded ? (
<GenericSkeletonLoader />
) : !canReadAuthSettings ? (
<NoPermission isFullPage resourceText="access your project's authentication settings" />
) : (
<AdvancedAuthSettingsForm />
)}
</ScaffoldContainer>
</>
)
interface AdvancedLayoutProps {
children: ReactNode
}
PageLayout.getLayout = (page) => {
export const AdvancedLayout = ({ children }: AdvancedLayoutProps) => {
const { ref } = useParams()
return (
<DefaultLayout>
<AuthLayout>{page}</AuthLayout>
<AuthLayout>
<PageLayout title="Advanced" subtitle="Configure advanced authentication server settings">
{children}
</PageLayout>
</AuthLayout>
</DefaultLayout>
)
}
export default PageLayout
const AdvancedPage: NextPageWithLayout = () => {
const isPermissionsLoaded = usePermissionsLoaded()
const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
if (isPermissionsLoaded && !canReadAuthSettings) {
return <NoPermission isFullPage resourceText="access your project's authentication settings" />
}
return (
<ScaffoldContainer>
{!isPermissionsLoaded ? (
<div className="mt-12">
<GenericSkeletonLoader />
</div>
) : (
<AdvancedAuthSettingsForm />
)}
</ScaffoldContainer>
)
}
AdvancedPage.getLayout = (page) => <AdvancedLayout>{page}</AdvancedLayout>
export default AdvancedPage
+31 -30
View File
@@ -1,48 +1,49 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ReactNode } from 'react'
import { HooksListing } from 'components/interfaces/Auth/Hooks/HooksListing'
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
import {
ScaffoldHeader,
ScaffoldContainer,
ScaffoldTitle,
ScaffoldDescription,
} from 'components/layouts/Scaffold'
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 { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import type { NextPageWithLayout } from 'types'
import DefaultLayout from 'components/layouts/DefaultLayout'
interface HooksLayoutProps {
children: ReactNode
}
export const HooksLayout = ({ children }: HooksLayoutProps) => {
return (
<DefaultLayout>
<AuthLayout>
<PageLayout
title="Auth Hooks"
subtitle="Use Postgres functions or HTTP endpoints to customize the behavior of Supabase Auth to meet your needs"
>
{children}
</PageLayout>
</AuthLayout>
</DefaultLayout>
)
}
const Hooks: NextPageWithLayout = () => {
const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const isPermissionsLoaded = usePermissionsLoaded()
if (isPermissionsLoaded && !canReadAuthSettings) {
return <NoPermission isFullPage resourceText="access your project's auth hooks" />
} else {
return (
<div>
<ScaffoldHeader className="pb-0">
<ScaffoldContainer id="auth-page-top">
<ScaffoldTitle>Auth Hooks</ScaffoldTitle>
<ScaffoldDescription>
Use Postgres functions or HTTP endpoints to customize the behavior of Supabase Auth to
meet your needs
</ScaffoldDescription>
</ScaffoldContainer>
</ScaffoldHeader>
<ScaffoldContainer className="my-8 space-y-8">
<HooksListing />
</ScaffoldContainer>
</div>
)
}
return (
<ScaffoldContainer>
<HooksListing />
</ScaffoldContainer>
)
}
Hooks.getLayout = (page) => (
<DefaultLayout>
<AuthLayout>{page}</AuthLayout>
</DefaultLayout>
)
Hooks.getLayout = (page) => <HooksLayout>{page}</HooksLayout>
export default Hooks
@@ -1,45 +1,59 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ReactNode } from 'react'
import {
AuthProvidersForm,
ProtectionAuthSettingsForm,
ThirdPartyAuthForm,
} from 'components/interfaces/Auth'
import { useParams } from 'common'
import { ProtectionAuthSettingsForm } from 'components/interfaces/Auth'
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldHeader, ScaffoldContainer, 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 canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const isPermissionsLoaded = usePermissionsLoaded()
if (isPermissionsLoaded && !canReadAuthSettings) {
return <NoPermission isFullPage resourceText="access your project's auth provider settings" />
} else {
return (
<div>
<ScaffoldHeader className="pb-0">
<ScaffoldContainer id="auth-page-top">
<ScaffoldTitle>Attack Protection</ScaffoldTitle>
</ScaffoldContainer>
</ScaffoldHeader>
<ScaffoldContainer className="my-8 space-y-8">
<ProtectionAuthSettingsForm />
</ScaffoldContainer>
</div>
)
}
interface ProtectionLayoutProps {
children: ReactNode
}
PageLayout.getLayout = (page) => {
export const ProtectionLayout = ({ children }: ProtectionLayoutProps) => {
const { ref } = useParams()
return (
<DefaultLayout>
<AuthLayout>{page}</AuthLayout>
<AuthLayout>
<PageLayout
title="Attack Protection"
subtitle="Configure security settings to protect your project from attacks"
>
{children}
</PageLayout>
</AuthLayout>
</DefaultLayout>
)
}
export default PageLayout
const ProtectionPage: NextPageWithLayout = () => {
const isPermissionsLoaded = usePermissionsLoaded()
const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
if (isPermissionsLoaded && !canReadAuthSettings) {
return <NoPermission isFullPage resourceText="access your project's auth provider settings" />
}
return (
<ScaffoldContainer>
{!isPermissionsLoaded ? (
<div className="mt-12">
<GenericSkeletonLoader />
</div>
) : (
<ProtectionAuthSettingsForm />
)}
</ScaffoldContainer>
)
}
ProtectionPage.getLayout = (page) => <ProtectionLayout>{page}</ProtectionLayout>
export default ProtectionPage
@@ -45,7 +45,7 @@ const ContainerVariants = cva('relative grid gap-10', {
horizontal: 'flex flex-col gap-2 md:grid md:grid-cols-12',
vertical: 'flex flex-col gap-3',
flex: 'flex flex-row gap-3',
'flex-row-reverse': 'flex flex-row gap-3 flex-row-reverse justify-between',
'flex-row-reverse': 'flex flex-row gap-6 flex-row-reverse justify-between',
},
flex: {
true: '',
@@ -233,7 +233,7 @@ const FlexContainer = cva('', {
},
{
layout: 'flex-row-reverse',
className: 'flex flex-col justify-center items-end',
className: 'flex flex-col justify-center items-end shrink-0',
},
],
})
+2
View File
@@ -58,6 +58,8 @@ export * from './src/components/Upload'
export * from './src/components/Form'
export * from './src/components/ExpandingTextArea'
export * from './src/components/PrePostTab'
// layout
export * from './src/components/LoadingLine'
@@ -0,0 +1,36 @@
import { forwardRef, ReactNode, PropsWithChildren, HTMLAttributes } from 'react'
import { cn } from '../../lib/utils'
export interface PrePostTabProps extends HTMLAttributes<HTMLDivElement> {
preTab?: ReactNode | string
postTab?: ReactNode | string
}
export const PrePostTab = forwardRef<HTMLDivElement, PropsWithChildren<PrePostTabProps>>(
({ preTab, postTab, children, className, ...props }, ref) => {
return (
<div ref={ref} {...props} className={cn('flex -space-x-px', className)}>
{preTab && (
<div className="border border-strong bg-surface-300 rounded-l-md px-3 flex items-center justify-center">
<span className="text-foreground-lighter text-xs font-mono">{preTab}</span>
</div>
)}
<div
className={cn('flex-1', {
'[&_input]:rounded-l-none [&_select]:rounded-l-none [&_textarea]:rounded-l-none':
Boolean(preTab),
'[&_input]:rounded-r-none [&_select]:rounded-r-none [&_textarea]:rounded-r-none':
Boolean(postTab),
})}
>
{children}
</div>
{postTab && (
<div className="border border-strong bg-surface-300 rounded-r-md px-3 flex items-center justify-center">
<span className="text-foreground-lighter text-xs font-mono">{postTab}</span>
</div>
)}
</div>
)
}
)
@@ -0,0 +1,2 @@
export { PrePostTab } from './PrePostTab'
export type { PrePostTabProps } from './PrePostTab'