This commit is contained in:
Saxon Fletcher committed 2025-02-11 10:17:44 +10:00
1 parent d349812c69
commit e7ad392b1d
5 files changed
+483 -510

No files matched your search

@@ -1,73 +1,24 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { toast } from 'sonner'
import { useParams } from 'common'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers/Shimmers'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { BASE_PATH } from 'lib/constants'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
WarningIcon,
Sheet,
SheetContent,
Badge,
SheetHeader,
SheetTitle,
SheetDescription,
SheetFooter,
Form_Shadcn_,
} from 'ui'
import { ResourceList } from 'components/ui/Resource/ResourceList'
import { ResourceItem } from 'components/ui/Resource/ResourceItem'
import { getPhoneProviderValidationSchema, PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation'
import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants'
import ProviderForm from './ProviderForm'
import { SectionHeader } from 'components/layouts/PageLayout'
import { DocsButton } from 'components/ui/DocsButton'
import type { Provider } from './AuthProvidersForm.types'
const generateInitialValues = (provider: Provider, config: any) => {
const doubleNegativeKeys = ['MAILER_AUTOCONFIRM', 'SMS_AUTOCONFIRM']
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 configValue = (config as any)[key]
initialValues[key] = configValue
? configValue
: provider.properties[key].type === 'boolean'
? false
: ''
}
}
})
return initialValues
}
const AuthProvidersForm = () => {
const { ref: projectRef } = useParams()
const [selectedProvider, setSelectedProvider] = useState<string | null>(null)
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
const {
isLoading,
error: authConfigError,
@@ -76,49 +27,6 @@ const AuthProvidersForm = () => {
isSuccess,
} = useAuthConfigQuery({ projectRef })
const activeProvider = selectedProvider
? PROVIDERS_SCHEMAS.find((provider) => provider.title === selectedProvider)
: null
const activeProviderSchema =
activeProvider?.title === 'Phone' && authConfig
? { ...activeProvider, validationSchema: getPhoneProviderValidationSchema(authConfig) }
: activeProvider
const form = useForm({
defaultValues: activeProviderSchema
? generateInitialValues(activeProviderSchema, authConfig)
: {},
})
useEffect(() => {
if (activeProviderSchema) {
form.reset(generateInitialValues(activeProviderSchema, authConfig))
}
}, [activeProviderSchema])
const onSubmit = (values: any) => {
const doubleNegativeKeys = ['MAILER_AUTOCONFIRM', 'SMS_AUTOCONFIRM']
const payload = { ...values }
Object.keys(values).map((x: string) => {
if (doubleNegativeKeys.includes(x)) payload[x] = !values[x]
if (payload[x] === '') payload[x] = null
})
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
onSuccess: () => {
setSelectedProvider(null)
toast.success('Successfully updated settings')
},
}
)
}
if (!authConfig) return null
return (
<div>
<SectionHeader
@@ -126,7 +34,7 @@ const AuthProvidersForm = () => {
subtitle="Authenticate your users through a suite of providers and login methods"
/>
<div className="space-y-4">
<div className="-space-y-px">
{authConfig?.EXTERNAL_EMAIL_ENABLED && authConfig?.MAILER_OTP_EXP > 3600 && (
<Alert_Shadcn_
className="flex w-full items-center justify-between my-3"
@@ -149,18 +57,17 @@ const AuthProvidersForm = () => {
</div>
</Alert_Shadcn_>
)}
{isLoading && (
<div className="space-y-2">
{PROVIDERS_SCHEMAS.map((provider) => (
{isLoading &&
PROVIDERS_SCHEMAS.map((provider) => {
return (
<div
key={`provider_${provider.title}`}
className="border rounded-md border-strong px-6 py-4"
className={[...ProviderCollapsibleClasses, 'px-6 py-3'].join(' ')}
>
<HorizontalShimmerWithIcon />
</div>
))}
</div>
)}
)
})}
{isError && (
<Alert_Shadcn_ variant="destructive">
<WarningIcon />
@@ -168,110 +75,21 @@ const AuthProvidersForm = () => {
<AlertDescription_Shadcn_>{authConfigError.message}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
{isSuccess && (
<ResourceList>
{PROVIDERS_SCHEMAS.map((provider) => {
const isSAMLEnabled = provider.title === 'SAML 2.0' && authConfig?.SAML_ENABLED
const isLinkedInOIDCEnabled =
provider.title === 'LinkedIn (OIDC)' && authConfig?.EXTERNAL_LINKEDIN_OIDC_ENABLED
const isSlackOIDCEnabled =
provider.title === 'Slack (OIDC)' && authConfig?.EXTERNAL_SLACK_OIDC_ENABLED
const isExternalProviderAndEnabled =
authConfig &&
(authConfig as any)[`EXTERNAL_${provider?.title?.toUpperCase()}_ENABLED`]
const isActive =
isSAMLEnabled ||
isExternalProviderAndEnabled ||
isLinkedInOIDCEnabled ||
isSlackOIDCEnabled
return (
<ResourceItem
key={`provider_${provider.title}`}
media={
<img
src={`${BASE_PATH}/img/icons/${provider.misc.iconKey}.svg`}
width={18}
alt={`${provider.title} auth icon`}
/>
}
meta={
<Badge variant={isActive ? 'success' : 'default'}>
{isActive ? 'Enabled' : 'Disabled'}
</Badge>
}
onClick={() => setSelectedProvider(provider.title)}
>
<div>
<div className="text-foreground">{provider.title}</div>
{'helper' in provider.misc && (
<p className="text-foreground-light text-xs">{provider.misc.helper}</p>
)}
</div>
</ResourceItem>
)
})}
</ResourceList>
)}
{isSuccess &&
PROVIDERS_SCHEMAS.map((provider) => {
const providerSchema =
provider.title === 'Phone'
? { ...provider, validationSchema: getPhoneProviderValidationSchema(authConfig) }
: provider
return (
<ProviderForm
key={`provider_${providerSchema.title}`}
config={authConfig}
provider={providerSchema as any}
/>
)
})}
</div>
<Sheet
open={selectedProvider !== null}
onOpenChange={(open) => !open && setSelectedProvider(null)}
>
<SheetContent size="lg" className="flex flex-col">
{activeProviderSchema && (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="h-full flex flex-col">
<SheetHeader className="shrink-0">
<SheetTitle className="flex items-center gap-3">
<img
src={`${BASE_PATH}/img/icons/${activeProviderSchema.misc.iconKey}.svg`}
width={21}
alt={`${activeProviderSchema.title} auth icon`}
/>
{activeProviderSchema.title} Configuration
</SheetTitle>
</SheetHeader>
<div className="px-6 py-4 flex-grow space-y-4 flex-1 overflow-auto">
<ProviderForm
config={authConfig}
provider={activeProviderSchema as any}
onClose={() => setSelectedProvider(null)}
form={form}
/>
</div>
<SheetFooter className="border-t px-6 py-4 shrink-0">
<div className="flex items-center justify-between w-full">
<DocsButton href={activeProviderSchema.link} />
<div className="flex items-center gap-x-3">
<Button
type="default"
onClick={() => {
form.reset()
setSelectedProvider(null)
}}
disabled={isUpdatingConfig}
>
Cancel
</Button>
<Button
type="primary"
htmlType="submit"
loading={isUpdatingConfig}
disabled={isUpdatingConfig}
>
Save changes
</Button>
</div>
</div>
</SheetFooter>
</form>
</Form_Shadcn_>
)}
</SheetContent>
</Sheet>
</div>
)
}
@@ -1,32 +1,19 @@
import dayjs from 'dayjs'
import { Eye, EyeOff, CalendarIcon } from 'lucide-react'
import { useState } from 'react'
import { BASE_PATH } from 'lib/constants'
import { format } from 'date-fns'
import { cn } from 'ui/src/lib/utils/cn'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import {
Button,
Input_Shadcn_,
Switch,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Popover_Shadcn_,
PopoverTrigger_Shadcn_,
PopoverContent_Shadcn_,
Calendar,
FormControl_Shadcn_,
TextArea_Shadcn_,
} from 'ui'
import { Markdown } from 'components/interfaces/Markdown'
import { DatePicker } from 'components/ui/DatePicker'
import { BASE_PATH } from 'lib/constants'
import { Button, Input, InputNumber, Listbox, Toggle } from 'ui'
import type { Enum } from './AuthProvidersForm.types'
interface FormFieldProps {
field: any
name: string
properties: any
formValues: any
setFieldValue: (field: string, v: any) => any
disabled?: boolean
}
@@ -34,151 +21,268 @@ function formatDate(date: Date): string {
return dayjs(date).format('dddd, MMMM D, YYYY HH:mm:ss Z')
}
const FormField = ({ field, name, properties, disabled = false }: FormFieldProps) => {
const FormField = ({
name,
properties,
formValues,
disabled = false,
setFieldValue,
}: FormFieldProps) => {
const [hidden, setHidden] = useState(!!properties.isSecret)
const [dateAsText, setDateAsText] = useState(
formValues[name] ? formatDate(new Date(formValues[name])) : ''
)
useEffect(() => {
if (properties.show && properties.show.key && !formValues[properties.show.key]) {
setFieldValue(name, '')
setDateAsText('')
}
}, [properties.show && properties.show.key && !formValues[properties.show.key]])
if (properties.show) {
if (properties.show.matches) {
if (!properties.show.matches.includes(properties.show.key)) {
if (!properties.show.matches.includes(formValues[properties.show.key])) {
return null
}
} else if (!properties.show.key) {
} else if (!formValues[properties.show.key]) {
return null
}
}
const renderMarkdown = (content?: string) => {
if (!content) return null
return (
<ReactMarkdown
className="text-foreground-light text-sm"
unwrapDisallowed
disallowedElements={['p']}
>
{content}
</ReactMarkdown>
)
}
switch (properties.type) {
case 'datetime':
return (
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<FormControl_Shadcn_>
<Button
type="outline"
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-foreground-light'
)}
disabled={disabled}
>
{field.value ? format(new Date(field.value), 'PPP') : <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl_Shadcn_>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value ? new Date(field.value) : undefined}
onSelect={(date: Date | undefined) => {
if (date) {
// Set time to 24 hours from now
date.setHours(date.getHours() + 24)
field.onChange(date.toISOString())
<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, '')
}
}}
disabled={(date: Date) => date < new Date()}
initialFocus
/>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
>
<span>Pick</span>
</DatePicker>
}
/>
)
case 'string':
return (
<div className="flex gap-2 flex-grow">
<Input_Shadcn_
{...field}
type={hidden ? 'password' : 'text'}
disabled={disabled}
value={field.value ?? ''}
/>
{properties.isSecret && (
<Button
type="default"
onClick={() => setHidden(!hidden)}
icon={hidden ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
/>
)}
{properties.units && (
<span className="flex items-center text-foreground-light text-sm">
{renderMarkdown(properties.units)}
</span>
)}
</div>
<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>
)
}
/>
)
case 'multiline-string':
return (
<div className="flex-grow">
<TextArea_Shadcn_
{...field}
className="resize-y"
rows={4}
disabled={disabled}
value={field.value ?? ''}
/>
{properties.units && (
<span className="flex items-center text-foreground-light text-sm mt-2">
{renderMarkdown(properties.units)}
</span>
)}
</div>
<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>
)
}
/>
)
case 'number':
return (
<div className="flex gap-2 flex-grow">
<Input_Shadcn_ {...field} type="number" disabled={disabled} value={field.value ?? ''} />
{properties.units && (
<span className="flex items-center text-foreground-light text-sm">
{renderMarkdown(properties.units)}
<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>
)}
</div>
}
/>
)
case 'boolean':
return (
<Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
<Toggle
size="small"
id={name}
name={name}
disabled={disabled}
label={properties.title}
descriptionText={
properties.description ? (
<ReactMarkdown
unwrapDisallowed
disallowedElements={['p']}
className="form-field-markdown"
>
{properties.description}
</ReactMarkdown>
) : null
}
/>
)
case 'select':
return (
<Select_Shadcn_ value={field.value} onValueChange={field.onChange} disabled={disabled}>
<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}>
<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 className="h-6 w-6" src={`${BASE_PATH}/img/icons/${option.icon}`} />
) : null
}}
>
{option.label}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</Listbox.Option>
)
})}
</Listbox>
)
default:
return <Input_Shadcn_ {...field} value={field.value ?? ''} disabled={disabled} />
break
}
return <></>
}
export default FormField
@@ -1,16 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Check, ChevronUp, ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'
import { useEffect, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
import { useForm, useWatch } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DocsButton } from 'components/ui/DocsButton'
import { FormFieldWrapper } from 'components/ui/Forms'
import type { components } from 'data/api'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
@@ -22,80 +19,34 @@ import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Button,
Form_Shadcn_,
Input_Shadcn_,
Switch,
Collapsible,
Form,
Input,
WarningIcon,
Card,
CardContent,
CardFooter,
} from 'ui'
import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants'
import type { Provider } from './AuthProvidersForm.types'
import FormField from './FormField'
import { Markdown } from 'components/interfaces/Markdown'
import { Admonition } from 'ui-patterns'
import { EyeIcon } from 'lucide-react'
import FormField from './FormField'
export interface ProviderFormProps {
config: components['schemas']['GoTrueConfigResponse']
provider: Provider
onClose: () => void
form: any
}
const generateInitialValues = (provider: Provider, config: ProviderFormProps['config']) => {
const doubleNegativeKeys = ['MAILER_AUTOCONFIRM', 'SMS_AUTOCONFIRM']
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 configValue = (config as any)[key]
initialValues[key] = configValue
? configValue
: provider.properties[key].type === 'boolean'
? false
: ''
}
}
})
return initialValues
}
const ProviderForm = ({ config, provider, onClose, form }: ProviderFormProps) => {
const ProviderForm = ({ config, provider }: ProviderFormProps) => {
const ref = useRef<HTMLButtonElement>(null)
const [open, setOpen] = useState(false)
const { ref: projectRef, provider: urlProvider } = useParams()
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
const doubleNegativeKeys = ['MAILER_AUTOCONFIRM', 'SMS_AUTOCONFIRM']
const canUpdateConfig: boolean = useCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const watchedValues = useWatch({
control: form.control,
name: Object.keys(provider.properties).map((key) => key),
})
const isSAMLEnabled: boolean =
provider.title === 'SAML 2.0' && config && (config as any)['SAML_ENABLED']
const isLinkedInOIDCEnabled: boolean =
provider.title === 'LinkedIn (OIDC)' &&
config &&
(config as any)['EXTERNAL_LINKEDIN_OIDC_ENABLED']
const isSlackOIDCEnabled =
provider.title === 'Slack (OIDC)' && config['EXTERNAL_SLACK_OIDC_ENABLED']
const isExternalProviderAndEnabled: boolean =
config && (config as any)[`EXTERNAL_${provider?.title?.toUpperCase()}_ENABLED`]
const isActive: boolean =
isSAMLEnabled || isExternalProviderAndEnabled || isLinkedInOIDCEnabled || isSlackOIDCEnabled
const shouldDisableField = (field: string): boolean => {
const shouldDisableSmsFields =
config.HOOK_SEND_SMS_ENABLED &&
@@ -115,13 +66,6 @@ const ProviderForm = ({ config, provider, onClose, form }: ProviderFormProps) =>
)
}
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
const apiUrl = `${protocol}://${endpoint}`
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const showAlert = (title: string) => {
switch (title) {
// TODO (KM): Remove after 10th October 2024 when we disable the provider
@@ -167,127 +111,229 @@ const ProviderForm = ({ config, provider, onClose, form }: ProviderFormProps) =>
}
}
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
const apiUrl = `${protocol}://${endpoint}`
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const generateInitialValues = () => {
const initialValues: { [x: string]: string | boolean } = {}
// the config is already loaded through the parent component
Object.keys(provider.properties).forEach((key) => {
// When the key is a 'double negative' key, we must reverse the boolean before adding it to the form
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 configValue = (config as any)[key]
initialValues[key] = configValue
? configValue
: provider.properties[key].type === 'boolean'
? false
: ''
}
}
})
return initialValues
}
const isSAMLEnabled: boolean =
provider.title === 'SAML 2.0' && config && (config as any)['SAML_ENABLED']
// [Joel] Introduced as the new LinkedIn provider has a corresponding config var of LINKEDIN_OIDC
const isLinkedInOIDCEnabled: boolean =
provider.title === 'LinkedIn (OIDC)' &&
config &&
(config as any)['EXTERNAL_LINKEDIN_OIDC_ENABLED']
const isSlackOIDCEnabled =
provider.title === 'Slack (OIDC)' && config['EXTERNAL_SLACK_OIDC_ENABLED']
const isExternalProviderAndEnabled: boolean =
config && (config as any)[`EXTERNAL_${provider?.title?.toUpperCase()}_ENABLED`]
// [Joshen] Doing this check as SAML doesn't follow the same naming structure as the other provider options
const isActive: boolean =
isSAMLEnabled || isExternalProviderAndEnabled || isLinkedInOIDCEnabled || isSlackOIDCEnabled
const INITIAL_VALUES = generateInitialValues()
const onSubmit = (values: any, { resetForm }: any) => {
const payload = { ...values }
// Format payload for the following checks:
// 1. Convert all empty string values to null
// 2. When the key is a 'double negative' key, we must reverse the boolean before the payload can be sent
Object.keys(values).map((x: string) => {
if (doubleNegativeKeys.includes(x)) payload[x] = !values[x]
if (payload[x] === '') payload[x] = null
})
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
onSuccess: () => {
resetForm({ values: { ...values }, initialValues: { ...values } })
setOpen(false)
toast.success('Successfully updated settings')
},
}
)
}
useEffect(() => {
if (urlProvider?.toLowerCase() === provider.title.toLowerCase()) {
onClose()
setOpen(true)
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlProvider])
return (
<div className="py-4">
{showAlert(provider.title)}
<Card>
<CardContent>
<div className="flex items-center justify-between space-x-2">
<div>
<p className="text-sm text-foreground">Provider Status</p>
<p className="text-sm text-foreground-light">
Enable or disable {provider.title} authentication for your users
</p>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-foreground-light">
{isActive ? 'Enabled' : 'Disabled'}
</span>
</div>
</div>
</CardContent>
{Object.keys(provider.properties).map((key) => {
const property = provider.properties[key]
if (!property) return null
// Check if field should be shown based on dependencies
if (property.show) {
const watchedValue = watchedValues[Object.keys(provider.properties).indexOf(key)]
if (property.show.matches) {
if (!property.show.matches.includes(watchedValue)) {
return null
}
} else if (!watchedValue) {
return null
}
}
return (
<CardContent key={key}>
<FormFieldWrapper
control={form.control}
name={key}
label={property.title || key}
description={property.description || ''}
orientation="horizontal"
>
{(field) => (
<FormField
field={field}
name={key}
properties={property}
disabled={shouldDisableField(key) || !canUpdateConfig}
/>
)}
</FormFieldWrapper>
</CardContent>
)
})}
{provider?.misc?.alert && (
<CardContent>
<Admonition
type="warning"
title={provider.misc.alert.title}
description={
<>
<ReactMarkdown>{provider.misc.alert.description}</ReactMarkdown>
</>
}
<Collapsible
open={open}
onOpenChange={setOpen}
className={ProviderCollapsibleClasses.join(' ')}
>
<Collapsible.Trigger asChild>
<button
ref={ref}
type="button"
className="group flex w-full items-center justify-between rounded py-3 px-4 text-foreground"
>
<div className="flex items-center gap-3">
<ChevronUp
className="text-border-stronger transition data-open-parent:rotate-0 data-closed-parent:rotate-180"
strokeWidth={2}
width={14}
/>
</CardContent>
)}
<img
src={`${BASE_PATH}/img/icons/${provider.misc.iconKey}.svg`}
width={18}
alt={`${provider.title} auth icon`}
/>
<span className="text-sm">{provider.title}</span>
</div>
<div className="flex items-center gap-3">
{isActive ? (
<div className="flex items-center gap-1 rounded-full border border-brand-400 bg-brand-200 py-1 px-1 text-xs text-brand">
<span className="rounded-full bg-brand p-0.5 text-xs text-brand-200">
<Check strokeWidth={2} size={12} />
</span>
<span className="px-1">Enabled</span>
</div>
) : (
<div className="rounded-md border border-strong bg-surface-100 py-1 px-3 text-xs text-foreground-lighter">
Disabled
</div>
)}
</div>
</button>
</Collapsible.Trigger>
<Form
name={`provider-${provider.title}-form`}
initialValues={INITIAL_VALUES}
validationSchema={provider.validationSchema}
onSubmit={onSubmit}
>
{({ handleReset, initialValues, values, setFieldValue }: any) => {
const noChanges = JSON.stringify(initialValues) === JSON.stringify(values)
return (
<Collapsible.Content>
<div className="group border-t border-strong bg-surface-100 py-6 px-4 md:px-6 text-foreground">
<div className="mx-auto my-2 md:my-6 max-w-lg space-y-6">
{showAlert(provider.title)}
{Object.keys(provider.properties).map((x: string) => (
<FormField
key={x}
name={x}
setFieldValue={setFieldValue}
properties={provider.properties[x]}
formValues={values}
disabled={
// TODO (KM): Remove after 10th October 2024 when we disable the provider
shouldDisableField(x) || !canUpdateConfig
}
/>
))}
{provider.misc.requiresRedirect && (
<CardContent>
<FormFieldWrapper
control={form.control}
name="callback_url"
label="Callback URL (for OAuth)"
description={
<Markdown content={provider.misc.helper} className="text-foreground-lighter" />
}
orientation="horizontal"
>
{() => (
<div className="flex gap-4">
<Input_Shadcn_
readOnly
disabled
value={
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}/auth/v1/callback`
: `${apiUrl}/auth/v1/callback`
}
/>
<Button
type="default"
onClick={() => {
const url =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}/auth/v1/callback`
: `${apiUrl}/auth/v1/callback`
navigator.clipboard.writeText(url)
toast.success('Copied to clipboard')
}}
>
Copy
</Button>
{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={
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}/auth/v1/callback`
: `${apiUrl}/auth/v1/callback`
}
descriptionText={
<Markdown
content={provider.misc.helper}
className="text-foreground-lighter"
/>
}
/>
</>
)}
<div className="flex items-center justify-between">
<DocsButton href={provider.link} />
<div className="flex items-center gap-x-3">
<Button
type="default"
htmlType="reset"
onClick={() => {
handleReset()
setOpen(false)
}}
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>
</div>
)}
</FormFieldWrapper>
</CardContent>
)}
</Card>
</div>
</div>
</Collapsible.Content>
)
}}
</Form>
</Collapsible>
)
}
@@ -239,11 +239,16 @@ const BasicAuthSettingsForm = () => {
</Alert_Shadcn_>
)}
</CardContent>
<CardFooter className="justify-end">
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!canUpdateConfig || isUpdatingConfig}
disabled={!canUpdateConfig || isUpdatingConfig || !form.formState.isDirty}
loading={isUpdatingConfig}
>
Save changes
@@ -42,7 +42,7 @@ export function FormFieldWrapper({
<FormDescription_Shadcn_ className="text-xs">{description}</FormDescription_Shadcn_>
)}
</div>
<div className={orientation === 'horizontal' ? 'col-span-6 flex justify-end' : ''}>
<div className={orientation === 'horizontal' ? 'col-span-6 flex flex-col items-end' : ''}>
<FormControl_Shadcn_>{children(field)}</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</div>