Files
supabase/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm.tsx
Saxon Fletcher 326d2cbdcb 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>
2025-03-04 09:58:25 +10:00

295 lines
10 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
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 { 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'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
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'
import { StringNumberOrNull } from 'components/ui/Forms/Form.constants'
const FormSchema = z.object({
API_MAX_REQUEST_DURATION: z.coerce
.number()
.min(5, 'Must be 5 or larger')
.max(30, 'Must be a value no greater than 30'),
DB_MAX_POOL_SIZE: StringNumberOrNull,
})
export const AdvancedAuthSettingsForm = () => {
const { ref: projectRef } = useParams()
const organization = useSelectedOrganization()
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,
} = useAuthConfigQuery({ projectRef })
const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({
orgSlug: organization?.slug,
})
const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
const isTeamsEnterprisePlan =
isSuccessSubscription && subscription?.plan.id !== 'free' && subscription?.plan.id !== 'pro'
const promptTeamsEnterpriseUpgrade =
IS_PLATFORM && isSuccessSubscription && !isTeamsEnterprisePlan
const requestDurationForm = useForm({
resolver: zodResolver(
z.object({
API_MAX_REQUEST_DURATION: FormSchema.shape.API_MAX_REQUEST_DURATION,
})
),
defaultValues: {
API_MAX_REQUEST_DURATION: 10,
},
})
const databaseForm = useForm({
resolver: zodResolver(
z.object({
DB_MAX_POOL_SIZE: FormSchema.shape.DB_MAX_POOL_SIZE,
})
),
defaultValues: {
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(
{ projectRef: projectRef, config: values },
{
onError: (error) => {
toast.error(`Failed to update request duration settings: ${error?.message}`)
setIsUpdatingRequestDurationForm(false)
},
onSuccess: () => {
toast.success('Successfully updated request duration settings')
setIsUpdatingRequestDurationForm(false)
},
}
)
}
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 (
<Alert_Shadcn_ variant="destructive">
<WarningIcon />
<AlertTitle_Shadcn_>Failed to retrieve auth configuration</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>{authConfigError.message}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)
}
if (!canReadConfig) {
return <NoPermission resourceText="view auth configuration settings" />
}
return (
<>
{promptTeamsEnterpriseUpgrade && (
<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>
)}
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">Request Duration</ScaffoldSectionTitle>
<Form_Shadcn_ {...requestDurationForm}>
<form
onSubmit={requestDurationForm.handleSubmit(onSubmitRequestDurationForm)}
className="space-y-4"
>
<Card>
<CardContent className="pt-6">
<FormField_Shadcn_
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">
<PrePostTab postTab="seconds">
<Input_Shadcn_
type="number"
min={5}
max={30}
{...field}
disabled={!canUpdateConfig || promptTeamsEnterpriseUpgrade}
/>
</PrePostTab>
</div>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<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={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"
placeholder="10"
{...field}
disabled={!canUpdateConfig || promptTeamsEnterpriseUpgrade}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</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>
</>
)
}