mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore: refactor auth provider form to use react-hook-form (#44095)
## Problem - The auth providers forms still use `formik` and we want to remove it in favour of `react-hook-form` to keep only one form library - The auth providers forms do not follow the design system guidelines ## Solution - Migrate to `react-hook-form` - Apply the design system guidelines ## Screenshots <img width="1530" height="1920" alt="image" src="https://github.com/user-attachments/assets/04627e93-2aa5-4a7f-a24e-0ae41d6e6b10" /> --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
This commit is contained in:
1 parent
a261bafbe2
commit
6b35cc8034
4 files changed
+499
-395
No files matched your search
@@ -1,52 +1,86 @@
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { DatePicker } from 'components/ui/DatePicker'
|
||||
import dayjs from 'dayjs'
|
||||
import { format } from 'date-fns'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { CalendarIcon, ExternalLink } from 'lucide-react'
|
||||
import { type Control } from 'react-hook-form'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Button, Input, InputNumber, Listbox, Toggle } from 'ui'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
PrePostTab,
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
Separator,
|
||||
SheetSection,
|
||||
Switch,
|
||||
Textarea,
|
||||
useWatch_Shadcn_,
|
||||
} from 'ui'
|
||||
import { Input as DataInput } from 'ui-patterns/DataInputs/Input'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
import type { Enum } from './AuthProvidersForm.types'
|
||||
|
||||
interface FormFieldProps {
|
||||
projectRef: string | undefined
|
||||
organizationSlug: string | undefined
|
||||
name: string
|
||||
properties: any
|
||||
formValues: any
|
||||
setFieldValue: (field: string, v: any) => any
|
||||
control: Control
|
||||
hasAccess: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return dayjs(date).format('dddd, MMMM D, YYYY HH:mm:ss Z')
|
||||
}
|
||||
|
||||
const FormField = ({
|
||||
projectRef,
|
||||
name,
|
||||
properties,
|
||||
formValues,
|
||||
disabled = false,
|
||||
setFieldValue,
|
||||
organizationSlug,
|
||||
control,
|
||||
hasAccess,
|
||||
disabled: disabledProp,
|
||||
}: FormFieldProps) => {
|
||||
const [hidden, setHidden] = useState(!!properties.isSecret)
|
||||
const [dateAsText, setDateAsText] = useState(
|
||||
formValues[name] ? formatDate(new Date(formValues[name])) : ''
|
||||
)
|
||||
const { description: originalDescription } = properties
|
||||
let description = originalDescription
|
||||
|
||||
useEffect(() => {
|
||||
if (properties.show && properties.show.key && !formValues[properties.show.key]) {
|
||||
setFieldValue(name, '')
|
||||
setDateAsText('')
|
||||
}
|
||||
}, [properties.show && properties.show.key && !formValues[properties.show.key]])
|
||||
if (originalDescription && projectRef) {
|
||||
description = originalDescription.replace(
|
||||
/\(\.\.\/auth\/(.*?)\)/g,
|
||||
`(/project/${projectRef}/auth/$1)`
|
||||
)
|
||||
}
|
||||
|
||||
const fieldValue = useWatch_Shadcn_({ control, name })
|
||||
if (!hasAccess) {
|
||||
const planMessage = organizationSlug
|
||||
? `Only available on [Pro plan](/org/${organizationSlug}/billing?panel=subscriptionPlan) and above.`
|
||||
: ''
|
||||
description = originalDescription ? `${originalDescription} ${planMessage}` : planMessage
|
||||
}
|
||||
const disabled =
|
||||
disabledProp || (properties.type === 'boolean' ? !hasAccess && !fieldValue : !hasAccess)
|
||||
|
||||
const showValue = useWatch_Shadcn_({
|
||||
control,
|
||||
name: properties.show?.key,
|
||||
disabled: properties.show == null,
|
||||
})
|
||||
|
||||
if (properties.show) {
|
||||
if (properties.show.matches) {
|
||||
if (!properties.show.matches.includes(formValues[properties.show.key])) {
|
||||
if (!properties.show.matches.includes(showValue)) {
|
||||
return null
|
||||
}
|
||||
} else if (!formValues[properties.show.key]) {
|
||||
} else if (!showValue) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -54,241 +88,304 @@ const FormField = ({
|
||||
switch (properties.type) {
|
||||
case 'datetime':
|
||||
return (
|
||||
<Input
|
||||
size="small"
|
||||
layout="vertical"
|
||||
id={name}
|
||||
name={name}
|
||||
type="text"
|
||||
value={dateAsText}
|
||||
readOnly
|
||||
label={properties.title}
|
||||
labelOptional={
|
||||
properties.descriptionOptional ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.descriptionOptional}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
descriptionText={
|
||||
properties.description ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.description}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
actions={
|
||||
<DatePicker
|
||||
selectsRange={false}
|
||||
minDate={new Date()}
|
||||
from={formValues[name]}
|
||||
to={formValues[name]}
|
||||
onChange={(date) => {
|
||||
if (date && date.to) {
|
||||
setFieldValue(name, date.to)
|
||||
setDateAsText(formatDate(new Date(date.to)))
|
||||
} else {
|
||||
setDateAsText('')
|
||||
setFieldValue(name, '')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>Pick</span>
|
||||
</DatePicker>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={control}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label={properties.title}
|
||||
description={
|
||||
description ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Popover_Shadcn_>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="outline"
|
||||
className="w-full justify-start text-left font-normal px-3 py-4"
|
||||
icon={<CalendarIcon className="h-4 w-4" />}
|
||||
size="small"
|
||||
>
|
||||
{field.value ? format(new Date(field.value), 'PPP') : 'Pick a date'}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date?.toISOString())
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator className="w-full" />
|
||||
</>
|
||||
)
|
||||
|
||||
case 'string':
|
||||
return (
|
||||
<Input
|
||||
size="small"
|
||||
layout="vertical"
|
||||
id={name}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
type={hidden ? 'password' : 'text'}
|
||||
label={properties.title}
|
||||
labelOptional={
|
||||
properties.descriptionOptional ? (
|
||||
<Markdown
|
||||
content={properties.descriptionOptional}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
descriptionText={
|
||||
properties.description ? (
|
||||
<Markdown content={properties.description} className="text-foreground-lighter" />
|
||||
) : null
|
||||
}
|
||||
actions={
|
||||
!!properties.isSecret ? (
|
||||
<Button
|
||||
icon={hidden ? <Eye /> : <EyeOff />}
|
||||
type="default"
|
||||
onClick={() => setHidden(!hidden)}
|
||||
/>
|
||||
) : (
|
||||
<span className="mr-3 text-foreground-lighter">
|
||||
{properties.units ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.units}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={control}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label={properties.title}
|
||||
description={
|
||||
description ? (
|
||||
<Markdown content={description} className="text-foreground-lighter" />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_ className="col-span-6">
|
||||
{properties.units ? (
|
||||
<PrePostTab
|
||||
postTab={
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.units}
|
||||
</ReactMarkdown>
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<DataInput
|
||||
{...field}
|
||||
type={properties.isSecret ? 'password' : 'text'}
|
||||
id={name}
|
||||
size="small"
|
||||
copy
|
||||
reveal
|
||||
/>
|
||||
</PrePostTab>
|
||||
) : (
|
||||
<DataInput
|
||||
{...field}
|
||||
type={properties.isSecret ? 'password' : 'text'}
|
||||
id={name}
|
||||
size="small"
|
||||
copy
|
||||
reveal
|
||||
/>
|
||||
)}
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator className="w-full" />
|
||||
</>
|
||||
)
|
||||
|
||||
case 'multiline-string':
|
||||
return (
|
||||
<Input.TextArea
|
||||
size="small"
|
||||
layout="vertical"
|
||||
id={name}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
type={hidden ? 'password' : 'text'}
|
||||
label={properties.title}
|
||||
labelOptional={
|
||||
properties.descriptionOptional ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.descriptionOptional}
|
||||
</ReactMarkdown>
|
||||
) : undefined
|
||||
}
|
||||
descriptionText={
|
||||
properties.description ? (
|
||||
<Markdown content={properties.description} className="text-foreground-lighter" />
|
||||
) : null
|
||||
}
|
||||
actions={
|
||||
!!properties.isSecret ? (
|
||||
<Button
|
||||
icon={hidden ? <Eye /> : <EyeOff />}
|
||||
type="default"
|
||||
onClick={() => setHidden(!hidden)}
|
||||
/>
|
||||
) : (
|
||||
<span className="mr-3 text-scale-900">
|
||||
{properties.units ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.units}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={control}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label={properties.title}
|
||||
description={
|
||||
description ? (
|
||||
<Markdown content={description} className="text-foreground-lighter" />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_ className="col-span-6">
|
||||
<Textarea
|
||||
{...field}
|
||||
id={name}
|
||||
rows={4}
|
||||
placeholder="Enter multi-line text"
|
||||
className="resize-none"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator className="w-full" />
|
||||
</>
|
||||
)
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<InputNumber
|
||||
size="small"
|
||||
layout="vertical"
|
||||
id={name}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
label={properties.title}
|
||||
labelOptional={
|
||||
properties.descriptionOptional ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.descriptionOptional}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
descriptionText={
|
||||
properties.description ? (
|
||||
<Markdown content={properties.description} className="text-foreground-lighter" />
|
||||
) : null
|
||||
}
|
||||
actions={
|
||||
<span className="mr-3 text-foreground-lighter">
|
||||
{properties.units ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.units}
|
||||
</ReactMarkdown>
|
||||
) : null}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={control}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label={properties.title}
|
||||
description={
|
||||
description ? (
|
||||
<Markdown content={description} className="text-foreground-lighter" />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_ className="col-span-6">
|
||||
{properties.units ? (
|
||||
<PrePostTab
|
||||
postTab={
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{properties.units}
|
||||
</ReactMarkdown>
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
id={name}
|
||||
type="number"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</PrePostTab>
|
||||
) : (
|
||||
<Input_Shadcn_
|
||||
{...field}
|
||||
id={name}
|
||||
type="number"
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
)}
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator className="w-full" />
|
||||
</>
|
||||
)
|
||||
|
||||
case 'boolean':
|
||||
return (
|
||||
<Toggle
|
||||
size="small"
|
||||
id={name}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
label={
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span>{properties.title}</span>
|
||||
{properties.link && (
|
||||
<a href={properties.link} target="_blank" rel="noreferrer noopener">
|
||||
<InfoTooltip side="bottom">Documentation</InfoTooltip>
|
||||
</a>
|
||||
<>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={control}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label={properties.title}
|
||||
description={
|
||||
<div className="flex flex-col gap-1">
|
||||
{description ? <Markdown content={description} /> : null}
|
||||
{properties.link && (
|
||||
<span>
|
||||
<Button asChild type="default" size="tiny" icon={<ExternalLink />}>
|
||||
<a href={properties.link} target="_blank" rel="noreferrer noopener">
|
||||
Documentation
|
||||
</a>
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_ className="col-span-6">
|
||||
<Switch
|
||||
id={name}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
size="small"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
descriptionText={
|
||||
properties.description ? <Markdown content={properties.description} /> : null
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator className="w-full" />
|
||||
</>
|
||||
)
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<Listbox
|
||||
size="small"
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
label={properties.title}
|
||||
descriptionText={
|
||||
properties.description ? (
|
||||
<ReactMarkdown
|
||||
unwrapDisallowed
|
||||
disallowedElements={['p']}
|
||||
className="form-field-markdown"
|
||||
>
|
||||
{properties.description}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
defaultValue={properties.enum[0]}
|
||||
>
|
||||
{properties.enum.map((option: Enum) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
id={option.value}
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
addOnBefore={() => {
|
||||
return option.icon ? (
|
||||
<img
|
||||
alt={`${option.label} icon`}
|
||||
className="h-6 w-6"
|
||||
src={`${BASE_PATH}/img/icons/${option.icon}`}
|
||||
/>
|
||||
) : null
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
<>
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={control}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label={properties.title}
|
||||
description={
|
||||
description ? (
|
||||
<ReactMarkdown
|
||||
unwrapDisallowed
|
||||
disallowedElements={['p']}
|
||||
className="form-field-markdown"
|
||||
>
|
||||
{description}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_ className="col-span-6">
|
||||
<Select_Shadcn_
|
||||
defaultValue={properties.enum[0]}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger_Shadcn_>
|
||||
<SelectValue_Shadcn_ placeholder="Select an option" />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{properties.enum.map((option: Enum) => (
|
||||
<SelectItem_Shadcn_ key={option.value} value={option.value}>
|
||||
<span className="flex gap-2 items-center">
|
||||
{option.icon ? (
|
||||
<img
|
||||
alt={`${option.label} icon`}
|
||||
className="h-6 w-6"
|
||||
src={`${BASE_PATH}/img/icons/${option.icon}`}
|
||||
/>
|
||||
) : null}
|
||||
{option.label}
|
||||
</span>
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
<Separator className="w-full" />
|
||||
</>
|
||||
)
|
||||
|
||||
default:
|
||||
break
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default FormField
|
||||
@@ -1,3 +1,4 @@
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
@@ -6,24 +7,38 @@ import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { ResourceItem } from 'components/ui/Resource/ResourceItem'
|
||||
import type { components } from 'data/api'
|
||||
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
|
||||
import { useHasEntitlementAccess } from 'hooks/misc/useCheckEntitlements'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useStaticEffectEvent } from 'hooks/useStaticEffectEvent'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { Check } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useId, useMemo, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { toast } from 'sonner'
|
||||
import { Button, Form, Input, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
import { NO_REQUIRED_CHARACTERS } from '../Auth.constants'
|
||||
import { AuthAlert } from './AuthAlert'
|
||||
import type { Provider } from './AuthProvidersForm.types'
|
||||
import FormField from './FormField'
|
||||
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
|
||||
|
||||
interface ProviderFormProps {
|
||||
config: components['schemas']['GoTrueConfigResponse']
|
||||
@@ -70,31 +85,36 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
|
||||
|
||||
const hasEntitlementAccess = useHasEntitlementAccess()
|
||||
|
||||
const INITIAL_VALUES = (() => {
|
||||
const initialValues: { [x: string]: string | boolean } = {}
|
||||
Object.keys(provider.properties).forEach((key) => {
|
||||
const isDoubleNegative = doubleNegativeKeys.includes(key)
|
||||
if (provider.title === 'SAML 2.0') {
|
||||
const configValue = (config as any)[key]
|
||||
initialValues[key] =
|
||||
configValue || (provider.properties[key].type === 'boolean' ? false : '')
|
||||
} else {
|
||||
if (isDoubleNegative) {
|
||||
initialValues[key] = !(config as any)[key]
|
||||
} else {
|
||||
const getValuesForProvider = useStaticEffectEvent(
|
||||
(config: components['schemas']['GoTrueConfigResponse']) => {
|
||||
const values: { [x: string]: string | boolean } = {}
|
||||
Object.keys(provider.properties).forEach((key) => {
|
||||
const isDoubleNegative = doubleNegativeKeys.includes(key)
|
||||
if (provider.title === 'SAML 2.0') {
|
||||
const configValue = (config as any)[key]
|
||||
initialValues[key] = configValue
|
||||
? configValue
|
||||
: provider.properties[key].type === 'boolean'
|
||||
? false
|
||||
: ''
|
||||
values[key] = configValue || (provider.properties[key].type === 'boolean' ? false : '')
|
||||
} else {
|
||||
if (isDoubleNegative) {
|
||||
values[key] = !(config as any)[key]
|
||||
} else {
|
||||
const configValue = (config as any)[key]
|
||||
values[key] = configValue
|
||||
? configValue
|
||||
: provider.properties[key].type === 'boolean'
|
||||
? false
|
||||
: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return initialValues
|
||||
})()
|
||||
})
|
||||
return values
|
||||
}
|
||||
)
|
||||
|
||||
const onSubmit = (values: any, { resetForm }: any) => {
|
||||
const INITIAL_VALUES = useMemo(() => {
|
||||
return getValuesForProvider(config)
|
||||
}, [config, getValuesForProvider])
|
||||
|
||||
const onSubmit = (values: any) => {
|
||||
const payload = { ...values }
|
||||
Object.keys(values).map((x: string) => {
|
||||
if (doubleNegativeKeys.includes(x)) payload[x] = !values[x]
|
||||
@@ -109,10 +129,10 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
|
||||
updateAuthConfig(
|
||||
{ projectRef: projectRef!, config: payload },
|
||||
{
|
||||
onSuccess: () => {
|
||||
resetForm({ values: { ...values }, initialValues: { ...values } })
|
||||
onSuccess: (newValues) => {
|
||||
setOpen(false)
|
||||
setUrlProvider(null)
|
||||
form.reset(getValuesForProvider(newValues))
|
||||
toast.success('Successfully updated settings')
|
||||
},
|
||||
}
|
||||
@@ -133,6 +153,19 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
|
||||
setOpen(isProviderInQuery)
|
||||
}, [urlProvider, provider.title])
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: INITIAL_VALUES,
|
||||
resolver: yupResolver(provider.validationSchema),
|
||||
shouldUnregister: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.reset(INITIAL_VALUES)
|
||||
}
|
||||
}, [open, form, INITIAL_VALUES])
|
||||
const formId = useId()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResourceItem
|
||||
@@ -164,7 +197,7 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
|
||||
</ResourceItem>
|
||||
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent className="flex flex-col gap-0">
|
||||
<SheetContent className="flex flex-col gap-0" size="lg">
|
||||
<SheetHeader className="shrink-0 flex items-center gap-4">
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/${provider.misc.iconKey}${provider.misc.hasLightIcon && !resolvedTheme?.includes('dark') ? '-light' : ''}.svg`}
|
||||
@@ -174,123 +207,99 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
|
||||
/>
|
||||
<SheetTitle>{provider.title}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<Form
|
||||
id={`provider-${provider.title}-form`}
|
||||
name={`provider-${provider.title}-form`}
|
||||
initialValues={INITIAL_VALUES}
|
||||
validationSchema={provider.validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
className="flex-1 overflow-hidden flex flex-col"
|
||||
>
|
||||
{({ handleReset, initialValues, values, setFieldValue }: any) => {
|
||||
const noChanges = JSON.stringify(initialValues) === JSON.stringify(values)
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 overflow-y-auto group py-6 px-4 md:px-6 text-foreground">
|
||||
<div className="mx-auto max-w-lg space-y-6">
|
||||
<AuthAlert
|
||||
title={provider.title}
|
||||
isHookSendSMSEnabled={config.HOOK_SEND_SMS_ENABLED}
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
name={formId}
|
||||
className="overflow-y-auto flex-grow px-0"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<AuthAlert
|
||||
title={provider.title}
|
||||
isHookSendSMSEnabled={config.HOOK_SEND_SMS_ENABLED}
|
||||
/>
|
||||
|
||||
{Object.keys(provider.properties).map((x: string) => {
|
||||
const { entitlementKey } = provider.properties[x]
|
||||
const hasAccess = entitlementKey == null || hasEntitlementAccess(entitlementKey)
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={x}
|
||||
projectRef={projectRef}
|
||||
organizationSlug={organization?.slug}
|
||||
name={x}
|
||||
properties={provider.properties[x]}
|
||||
control={form.control}
|
||||
disabled={shouldDisableField(x) || !canUpdateConfig}
|
||||
hasAccess={hasAccess}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{provider?.misc?.alert && (
|
||||
<SheetSection>
|
||||
<Admonition
|
||||
type="warning"
|
||||
title={provider.misc.alert.title}
|
||||
description={<ReactMarkdown>{provider.misc.alert.description}</ReactMarkdown>}
|
||||
/>
|
||||
</SheetSection>
|
||||
)}
|
||||
|
||||
{provider.misc.requiresRedirect && (
|
||||
<SheetSection>
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label="Callback URL (for OAuth)"
|
||||
description={
|
||||
<Markdown
|
||||
content={provider.misc.helper}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
|
||||
{Object.keys(provider.properties).map((x: string) => {
|
||||
let description = provider.properties[x].description
|
||||
if (description && projectRef) {
|
||||
description = description.replace(
|
||||
/\(\.\.\/auth\/(.*?)\)/g,
|
||||
`(/project/${projectRef}/auth/$1)`
|
||||
)
|
||||
}
|
||||
|
||||
const { entitlementKey } = provider.properties[x]
|
||||
const hasAccess =
|
||||
entitlementKey == null || hasEntitlementAccess(entitlementKey)
|
||||
const properties = {
|
||||
...provider.properties[x],
|
||||
description: hasAccess
|
||||
? description
|
||||
: `${description} Only available on [Pro plan](/org/${organization?.slug}/billing?panel=subscriptionPlan) and above.`,
|
||||
}
|
||||
const shouldDisable =
|
||||
properties.type === 'boolean' ? !hasAccess && !values[x] : !hasAccess
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={x}
|
||||
name={x}
|
||||
setFieldValue={setFieldValue}
|
||||
properties={properties}
|
||||
formValues={values}
|
||||
disabled={shouldDisableField(x) || !canUpdateConfig || shouldDisable}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{provider?.misc?.alert && (
|
||||
<Admonition
|
||||
type="warning"
|
||||
title={provider.misc.alert.title}
|
||||
description={
|
||||
<ReactMarkdown>{provider.misc.alert.description}</ReactMarkdown>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{provider.misc.requiresRedirect && (
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
label="Callback URL (for OAuth)"
|
||||
value={`${endpoint}/auth/v1/callback`}
|
||||
descriptionText={
|
||||
<Markdown
|
||||
content={provider.misc.helper}
|
||||
className="text-foreground-lighter"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter className="shrink-0">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<DocsButton href={provider.link} />
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="reset"
|
||||
onClick={() => {
|
||||
handleReset()
|
||||
setOpen(false)
|
||||
setUrlProvider(null)
|
||||
}}
|
||||
disabled={isUpdatingConfig}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<ButtonTooltip
|
||||
htmlType="submit"
|
||||
loading={isUpdatingConfig}
|
||||
disabled={isUpdatingConfig || !canUpdateConfig || noChanges}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canUpdateConfig
|
||||
? 'You need additional permissions to update provider settings'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</ButtonTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
<Input copy readOnly value={endpoint ? `${endpoint}/auth/v1/callback` : ''} />
|
||||
</FormItemLayout>
|
||||
</SheetSection>
|
||||
)}
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
<SheetFooter className="shrink-0">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<DocsButton href={provider.link} />
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="reset"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setUrlProvider(null)
|
||||
form.reset()
|
||||
}}
|
||||
disabled={isUpdatingConfig}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<ButtonTooltip
|
||||
form={formId}
|
||||
htmlType="submit"
|
||||
loading={isUpdatingConfig}
|
||||
disabled={isUpdatingConfig || !canUpdateConfig || !form.formState.isDirty}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canUpdateConfig
|
||||
? 'You need additional permissions to update provider settings'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</ButtonTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
|
||||
@@ -16,8 +16,8 @@ const PROVIDER_EMAIL = {
|
||||
link: `${DOCS_URL}/guides/auth/passwords`,
|
||||
properties: {
|
||||
EXTERNAL_EMAIL_ENABLED: {
|
||||
title: 'Enable Email provider',
|
||||
description: 'This will enable Email based signup and login for your application',
|
||||
title: 'Enable email provider',
|
||||
description: 'Allow email-based sign up and log in for your application.',
|
||||
type: 'boolean',
|
||||
},
|
||||
MAILER_SECURE_EMAIL_CHANGE_ENABLED: {
|
||||
@@ -34,22 +34,20 @@ const PROVIDER_EMAIL = {
|
||||
},
|
||||
PASSWORD_HIBP_ENABLED: {
|
||||
title: 'Prevent use of leaked passwords',
|
||||
description:
|
||||
'Rejects the use of known or easy to guess passwords on sign up or password change. Powered by the HaveIBeenPwned.org Pwned Passwords API.',
|
||||
description: `Rejects the use of known or easy to guess passwords on sign up or password change. Powered by the HaveIBeenPwned.org Pwned Passwords API. [Learn more](${DOCS_URL}/guides/auth/password-security#password-strength-and-leaked-password-protection)`,
|
||||
type: 'boolean',
|
||||
link: `${DOCS_URL}/guides/auth/password-security#password-strength-and-leaked-password-protection`,
|
||||
entitlementKey: 'auth.password_hibp',
|
||||
},
|
||||
PASSWORD_MIN_LENGTH: {
|
||||
title: 'Minimum password length',
|
||||
type: 'number',
|
||||
description:
|
||||
'Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.',
|
||||
'Passwords shorter than this value will be rejected as weak. Minimum 6 characters, though 8 or more is recommended.',
|
||||
units: 'characters',
|
||||
},
|
||||
PASSWORD_REQUIRED_CHARACTERS: {
|
||||
type: 'select',
|
||||
title: 'Password Requirements',
|
||||
title: 'Password requirements',
|
||||
description: 'Passwords that do not have at least one of each will be rejected as weak.',
|
||||
enum: [
|
||||
{
|
||||
@@ -73,15 +71,15 @@ const PROVIDER_EMAIL = {
|
||||
},
|
||||
|
||||
MAILER_OTP_EXP: {
|
||||
title: 'Email OTP Expiration',
|
||||
title: 'Email OTP expiration',
|
||||
type: 'number',
|
||||
description: 'Duration before an email otp / link expires.',
|
||||
description: 'Duration before an email OTP / link expires.',
|
||||
units: 'seconds',
|
||||
},
|
||||
MAILER_OTP_LENGTH: {
|
||||
title: 'Email OTP Length',
|
||||
title: 'Email OTP length',
|
||||
type: 'number',
|
||||
description: 'Number of digits in the email OTP',
|
||||
description: 'Number of digits in the email OTP.',
|
||||
units: 'number',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -372,7 +372,7 @@ export const FormLayout = React.forwardRef<
|
||||
>
|
||||
{hasLabel && isReactForm ? (
|
||||
<FormLabel_Shadcn_
|
||||
className="text-foreground flex gap-2 items-center break-all"
|
||||
className="text-foreground flex gap-2 items-center break-words"
|
||||
data-formlayout-id="formLabel"
|
||||
htmlFor={props.name || id}
|
||||
>
|
||||
@@ -380,7 +380,7 @@ export const FormLayout = React.forwardRef<
|
||||
</FormLabel_Shadcn_>
|
||||
) : (
|
||||
<Label_Shadcn_
|
||||
className="text-foreground flex gap-2 items-center break-all leading-normal"
|
||||
className="text-foreground flex gap-2 items-center break-words leading-normal"
|
||||
data-formlayout-id="label"
|
||||
htmlFor={props.name || id}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user