mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## 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 -->
356 lines
13 KiB
TypeScript
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>
|
|
)
|
|
}
|