BasicAuthSettingsForm.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import { ExternalLink } from 'lucide-react'
  5. import Link from 'next/link'
  6. import { useEffect } from 'react'
  7. import { useForm } from 'react-hook-form'
  8. import { toast } from 'sonner'
  9. import {
  10. Alert,
  11. AlertDescription,
  12. AlertTitle,
  13. Button,
  14. Card,
  15. CardContent,
  16. CardFooter,
  17. Form,
  18. FormControl,
  19. FormField,
  20. Switch,
  21. WarningIcon,
  22. } from 'ui'
  23. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  24. import {
  25. PageSection,
  26. PageSectionContent,
  27. PageSectionMeta,
  28. PageSectionSummary,
  29. PageSectionTitle,
  30. } from 'ui-patterns/PageSection'
  31. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  32. import * as z from 'zod'
  33. import { NO_REQUIRED_CHARACTERS } from './Auth.constants'
  34. import AlertError from '@/components/ui/AlertError'
  35. import { InlineLink } from '@/components/ui/InlineLink'
  36. import NoPermission from '@/components/ui/NoPermission'
  37. import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
  38. import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
  39. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  40. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  41. import { DOCS_URL } from '@/lib/constants'
  42. const schema = z.object({
  43. DISABLE_SIGNUP: z.boolean(),
  44. EXTERNAL_ANONYMOUS_USERS_ENABLED: z.boolean(),
  45. SECURITY_MANUAL_LINKING_ENABLED: z.boolean(),
  46. MAILER_AUTOCONFIRM: z.boolean(),
  47. SITE_URL: z.string().min(1, 'Must have a Site URL'),
  48. })
  49. export const BasicAuthSettingsForm = () => {
  50. const { ref: projectRef } = useParams()
  51. const showManualLinking = useIsFeatureEnabled('authentication:show_manual_linking')
  52. const {
  53. data: authConfig,
  54. error: authConfigError,
  55. isError,
  56. isSuccess,
  57. isPending: isLoading,
  58. } = useAuthConfigQuery({ projectRef })
  59. const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation()
  60. const { can: canReadConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
  61. PermissionAction.READ,
  62. 'custom_config_gotrue'
  63. )
  64. const { can: canUpdateConfig } = useAsyncCheckPermissions(
  65. PermissionAction.UPDATE,
  66. 'custom_config_gotrue'
  67. )
  68. const form = useForm({
  69. resolver: zodResolver(schema as any),
  70. defaultValues: {
  71. DISABLE_SIGNUP: true,
  72. EXTERNAL_ANONYMOUS_USERS_ENABLED: false,
  73. SECURITY_MANUAL_LINKING_ENABLED: false,
  74. MAILER_AUTOCONFIRM: true,
  75. SITE_URL: '',
  76. },
  77. })
  78. const { isDirty } = form.formState
  79. useEffect(() => {
  80. if (authConfig) {
  81. form.reset({
  82. DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP,
  83. EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED,
  84. SECURITY_MANUAL_LINKING_ENABLED: authConfig.SECURITY_MANUAL_LINKING_ENABLED,
  85. // The backend uses false to represent that email confirmation is required
  86. MAILER_AUTOCONFIRM: !authConfig.MAILER_AUTOCONFIRM,
  87. SITE_URL: authConfig.SITE_URL,
  88. })
  89. }
  90. }, [authConfig])
  91. const onSubmit = (values: any) => {
  92. const payload = { ...values }
  93. payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP
  94. // The backend uses empty string to represent no required characters in the password
  95. if (payload.PASSWORD_REQUIRED_CHARACTERS === NO_REQUIRED_CHARACTERS) {
  96. payload.PASSWORD_REQUIRED_CHARACTERS = ''
  97. }
  98. // The backend uses false to represent that email confirmation is required
  99. payload.MAILER_AUTOCONFIRM = !values.MAILER_AUTOCONFIRM
  100. updateAuthConfig(
  101. { projectRef: projectRef!, config: payload },
  102. {
  103. onError: (error) => {
  104. toast.error(`Failed to update settings: ${error?.message}`)
  105. },
  106. onSuccess: () => {
  107. toast.success('Successfully updated settings')
  108. },
  109. }
  110. )
  111. }
  112. return (
  113. <PageSection>
  114. <PageSectionMeta>
  115. <PageSectionSummary>
  116. <PageSectionTitle>User Signups</PageSectionTitle>
  117. </PageSectionSummary>
  118. </PageSectionMeta>
  119. <PageSectionContent>
  120. {isError && (
  121. <AlertError
  122. error={authConfigError}
  123. subject="Failed to retrieve auth configuration for hooks"
  124. />
  125. )}
  126. {isPermissionsLoaded && !canReadConfig && (
  127. <div className="mt-8">
  128. <NoPermission resourceText="view auth configuration settings" />
  129. </div>
  130. )}
  131. {isLoading && (
  132. <Card>
  133. <CardContent className="py-6">
  134. <ShimmeringLoader />
  135. </CardContent>
  136. <CardContent className="py-6">
  137. <ShimmeringLoader />
  138. </CardContent>
  139. <CardContent className="py-6">
  140. <ShimmeringLoader />
  141. </CardContent>
  142. <CardContent className="py-7">
  143. <ShimmeringLoader />
  144. </CardContent>
  145. <CardContent className="py-7"></CardContent>
  146. </Card>
  147. )}
  148. {isSuccess && (
  149. <Form {...form}>
  150. <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
  151. <Card>
  152. <CardContent>
  153. <FormField
  154. control={form.control}
  155. name="DISABLE_SIGNUP"
  156. render={({ field }) => (
  157. <FormItemLayout
  158. layout="flex-row-reverse"
  159. label="Allow new users to sign up"
  160. description="If this is disabled, new users will not be able to sign up to your application"
  161. >
  162. <FormControl>
  163. <Switch
  164. checked={field.value}
  165. onCheckedChange={field.onChange}
  166. disabled={!canUpdateConfig}
  167. />
  168. </FormControl>
  169. </FormItemLayout>
  170. )}
  171. />
  172. </CardContent>
  173. {showManualLinking && (
  174. <CardContent>
  175. <FormField
  176. control={form.control}
  177. name="SECURITY_MANUAL_LINKING_ENABLED"
  178. render={({ field }) => (
  179. <FormItemLayout
  180. layout="flex-row-reverse"
  181. label="Allow manual linking"
  182. description={
  183. <>
  184. Enable{' '}
  185. <InlineLink
  186. className="text-foreground-light hover:text-foreground"
  187. href={`${DOCS_URL}/guides/auth/auth-identity-linking#manual-linking-beta`}
  188. >
  189. manual linking APIs
  190. </InlineLink>{' '}
  191. for your project
  192. </>
  193. }
  194. >
  195. <FormControl>
  196. <Switch
  197. checked={field.value}
  198. onCheckedChange={field.onChange}
  199. disabled={!canUpdateConfig}
  200. />
  201. </FormControl>
  202. </FormItemLayout>
  203. )}
  204. />
  205. </CardContent>
  206. )}
  207. <CardContent>
  208. <FormField
  209. control={form.control}
  210. name="EXTERNAL_ANONYMOUS_USERS_ENABLED"
  211. render={({ field }) => (
  212. <FormItemLayout
  213. layout="flex-row-reverse"
  214. label="Allow anonymous sign-ins"
  215. description={
  216. <>
  217. Enable{' '}
  218. <InlineLink
  219. className="text-foreground-light hover:text-foreground"
  220. href={`${DOCS_URL}/guides/auth/auth-anonymous`}
  221. >
  222. anonymous sign-ins
  223. </InlineLink>{' '}
  224. for your project
  225. </>
  226. }
  227. >
  228. <FormControl>
  229. <Switch
  230. checked={field.value}
  231. onCheckedChange={field.onChange}
  232. disabled={!canUpdateConfig}
  233. />
  234. </FormControl>
  235. </FormItemLayout>
  236. )}
  237. />
  238. {form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
  239. <Alert
  240. className="flex w-full items-center justify-between mt-4"
  241. variant="warning"
  242. >
  243. <WarningIcon />
  244. <div>
  245. <AlertTitle>
  246. Anonymous users will use the{' '}
  247. <code className="text-code-inline">authenticated</code> role when signing
  248. in
  249. </AlertTitle>
  250. <AlertDescription className="flex flex-col gap-y-3">
  251. <p>
  252. As a result, anonymous users will be subjected to RLS policies that
  253. apply to the <code className="text-code-inline">public</code> and{' '}
  254. <code className="text-code-inline">authenticated</code> roles. We
  255. strongly advise{' '}
  256. <Link
  257. href={`/project/${projectRef}/auth/policies`}
  258. className="text-foreground underline"
  259. >
  260. reviewing your RLS policies
  261. </Link>{' '}
  262. to ensure that access to your data is restricted where required.
  263. </p>
  264. <Button asChild type="default" className="w-min" icon={<ExternalLink />}>
  265. <Link href={`${DOCS_URL}/guides/auth/auth-anonymous#access-control`}>
  266. View access control docs
  267. </Link>
  268. </Button>
  269. </AlertDescription>
  270. </div>
  271. </Alert>
  272. )}
  273. {!authConfig?.SECURITY_CAPTCHA_ENABLED &&
  274. form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
  275. <Alert className="mt-4">
  276. <WarningIcon />
  277. <AlertTitle>
  278. We highly recommend{' '}
  279. <InlineLink href={`/project/${projectRef}/auth/protection`}>
  280. enabling captcha
  281. </InlineLink>{' '}
  282. for anonymous sign-ins
  283. </AlertTitle>
  284. <AlertDescription>
  285. This will prevent potential abuse on sign-ins which may bloat your
  286. database and incur costs for monthly active users (MAU)
  287. </AlertDescription>
  288. </Alert>
  289. )}
  290. </CardContent>
  291. <CardContent>
  292. <FormField
  293. control={form.control}
  294. name="MAILER_AUTOCONFIRM"
  295. render={({ field }) => (
  296. <FormItemLayout
  297. layout="flex-row-reverse"
  298. label="Confirm email"
  299. description="Users will need to confirm their email address before signing in for the first time"
  300. >
  301. <FormControl>
  302. <Switch
  303. checked={field.value}
  304. onCheckedChange={field.onChange}
  305. disabled={!canUpdateConfig}
  306. />
  307. </FormControl>
  308. </FormItemLayout>
  309. )}
  310. />
  311. </CardContent>
  312. <CardFooter className="justify-end space-x-2">
  313. {isDirty && (
  314. <Button type="default" onClick={() => form.reset()}>
  315. Cancel
  316. </Button>
  317. )}
  318. <Button
  319. type="primary"
  320. htmlType="submit"
  321. disabled={!canUpdateConfig || isUpdatingConfig || !isDirty}
  322. loading={isUpdatingConfig}
  323. >
  324. Save changes
  325. </Button>
  326. </CardFooter>
  327. </Card>
  328. </form>
  329. </Form>
  330. )}
  331. </PageSectionContent>
  332. </PageSection>
  333. )
  334. }