| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- 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 as any),
- 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}/auth/policies`}
- className="text-foreground underline"
- >
- reviewing your RLS policies
- </Link>{' '}
- to ensure that access to your data is restricted where required.
- </p>
- <Button asChild type="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 type="default" onClick={() => form.reset()}>
- Cancel
- </Button>
- )}
- <Button
- type="primary"
- htmlType="submit"
- disabled={!canUpdateConfig || isUpdatingConfig || !isDirty}
- loading={isUpdatingConfig}
- >
- Save changes
- </Button>
- </CardFooter>
- </Card>
- </form>
- </Form>
- )}
- </PageSectionContent>
- </PageSection>
- )
- }
|