Files
supabase/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx
T
Joshen Lim 1de298ff31 Reinstate https://github.com/supabase/supabase/pull/45143 into latest master (#47433)
## Context

Previous PR was [here](https://github.com/supabase/supabase/pull/45143)
but it got stale with lots of conflicts so figured it'll be easier redo
it off the latest master

Moves policies page from Auth to Database under an Access Control
section along with Roles. This moves all existing files, applies
redirects, and updates urls to point to the new route

<img width="274" height="412" alt="image"
src="https://github.com/user-attachments/assets/7952c185-64ae-4355-ba36-45397efe1787"
/>

<img width="453" height="471" alt="image"
src="https://github.com/user-attachments/assets/04b3dcb3-48a5-4049-9893-d01109fb46a9"
/>


## To test
- [ ] Verify that policies now live under Database correctly

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a quick navigation shortcut to open **Database > Policies
(RLS)**.
* **Bug Fixes**
* Updated Policies and RLS-related links across the product to open the
**Database policies** area (menus, command palette, context actions,
alerts, and link-outs).
* Added a permanent redirect from the old **auth policies** URL to the
new **database policies** URL.
* **Documentation**
* Updated RLS Dashboard and security checklist instructions to reference
**Database > Policies**.
* **Tests**
  * Adjusted automated tests to validate the new Policies route.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-30 18:49:33 +08:00

356 lines
13 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Alert,
AlertDescription,
AlertTitle,
Button,
Card,
CardContent,
CardFooter,
Form,
FormControl,
FormField,
Switch,
WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
PageSection,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'
import { NO_REQUIRED_CHARACTERS } from './Auth.constants'
import { AlertError } from '@/components/ui/AlertError'
import { InlineLink } from '@/components/ui/InlineLink'
import { NoPermission } from '@/components/ui/NoPermission'
import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { DOCS_URL } from '@/lib/constants'
const schema = z.object({
DISABLE_SIGNUP: z.boolean(),
EXTERNAL_ANONYMOUS_USERS_ENABLED: z.boolean(),
SECURITY_MANUAL_LINKING_ENABLED: z.boolean(),
MAILER_AUTOCONFIRM: z.boolean(),
SITE_URL: z.string().min(1, 'Must have a Site URL'),
})
export const BasicAuthSettingsForm = () => {
const { ref: projectRef } = useParams()
const showManualLinking = useIsFeatureEnabled('authentication:show_manual_linking')
const {
data: authConfig,
error: authConfigError,
isError,
isSuccess,
isPending: isLoading,
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation()
const { can: canReadConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
DISABLE_SIGNUP: true,
EXTERNAL_ANONYMOUS_USERS_ENABLED: false,
SECURITY_MANUAL_LINKING_ENABLED: false,
MAILER_AUTOCONFIRM: true,
SITE_URL: '',
},
})
const { isDirty } = form.formState
useEffect(() => {
if (authConfig) {
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,
// The backend uses false to represent that email confirmation is required
MAILER_AUTOCONFIRM: !authConfig.MAILER_AUTOCONFIRM,
SITE_URL: authConfig.SITE_URL,
})
}
}, [authConfig])
const onSubmit = (values: any) => {
const payload = { ...values }
payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP
// The backend uses empty string to represent no required characters in the password
if (payload.PASSWORD_REQUIRED_CHARACTERS === NO_REQUIRED_CHARACTERS) {
payload.PASSWORD_REQUIRED_CHARACTERS = ''
}
// The backend uses false to represent that email confirmation is required
payload.MAILER_AUTOCONFIRM = !values.MAILER_AUTOCONFIRM
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
onError: (error) => {
toast.error(`Failed to update settings: ${error?.message}`)
},
onSuccess: () => {
toast.success('Successfully updated settings')
},
}
)
}
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>User Signups</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
{isError && (
<AlertError
error={authConfigError}
subject="Failed to retrieve auth configuration for hooks"
/>
)}
{isPermissionsLoaded && !canReadConfig && (
<div className="mt-8">
<NoPermission resourceText="view auth configuration settings" />
</div>
)}
{isLoading && (
<Card>
<CardContent className="py-6">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-6">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-6">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-7">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-7"></CardContent>
</Card>
)}
{isSuccess && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardContent>
<FormField
control={form.control}
name="DISABLE_SIGNUP"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow new users to sign up"
description="If this is disabled, new users will not be able to sign up to your application"
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
{showManualLinking && (
<CardContent>
<FormField
control={form.control}
name="SECURITY_MANUAL_LINKING_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow manual linking"
description={
<>
Enable{' '}
<InlineLink
className="text-foreground-light hover:text-foreground"
href={`${DOCS_URL}/guides/auth/auth-identity-linking#manual-linking-beta`}
>
manual linking APIs
</InlineLink>{' '}
for your project
</>
}
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
)}
<CardContent>
<FormField
control={form.control}
name="EXTERNAL_ANONYMOUS_USERS_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow anonymous sign-ins"
description={
<>
Enable{' '}
<InlineLink
className="text-foreground-light hover:text-foreground"
href={`${DOCS_URL}/guides/auth/auth-anonymous`}
>
anonymous sign-ins
</InlineLink>{' '}
for your project
</>
}
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}
/>
{form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
<Alert
className="flex w-full items-center justify-between mt-4"
variant="warning"
>
<WarningIcon />
<div>
<AlertTitle>
Anonymous users will use the{' '}
<code className="text-code-inline">authenticated</code> role when signing
in
</AlertTitle>
<AlertDescription className="flex flex-col gap-y-3">
<p>
As a result, anonymous users will be subjected to RLS policies that
apply to the <code className="text-code-inline">public</code> and{' '}
<code className="text-code-inline">authenticated</code> roles. We
strongly advise{' '}
<Link
href={`/project/${projectRef}/database/policies`}
className="text-foreground underline"
>
reviewing your RLS policies
</Link>{' '}
to ensure that access to your data is restricted where required.
</p>
<Button
asChild
variant="default"
className="w-min"
icon={<ExternalLink />}
>
<Link href={`${DOCS_URL}/guides/auth/auth-anonymous#access-control`}>
View access control docs
</Link>
</Button>
</AlertDescription>
</div>
</Alert>
)}
{!authConfig?.SECURITY_CAPTCHA_ENABLED &&
form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
<Alert className="mt-4">
<WarningIcon />
<AlertTitle>
We highly recommend{' '}
<InlineLink href={`/project/${projectRef}/auth/protection`}>
enabling captcha
</InlineLink>{' '}
for anonymous sign-ins
</AlertTitle>
<AlertDescription>
This will prevent potential abuse on sign-ins which may bloat your
database and incur costs for monthly active users (MAU)
</AlertDescription>
</Alert>
)}
</CardContent>
<CardContent>
<FormField
control={form.control}
name="MAILER_AUTOCONFIRM"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Confirm email"
description="Users will need to confirm their email address before signing in for the first time"
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{isDirty && (
<Button variant="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
<Button
variant="primary"
type="submit"
disabled={!canUpdateConfig || isUpdatingConfig || !isDirty}
loading={isUpdatingConfig}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
)}
</PageSectionContent>
</PageSection>
)
}