Files
supabase/apps/studio/components/interfaces/Auth/OAuthApps/OAuthServerSettingsForm.tsx
T
Gildas Garcia 0facd341a6 chore: remove UI form components _Shadcn_ suffix (#45212)
## Problem

We used to have a `_Shadcn_` suffix for all the shadcn form components
because we also had `formik` form components.
This is not needed anymore.

## Solution

- Remove the suffix
- Update all usages
2026-04-24 12:14:15 +02:00

436 lines
16 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Card,
CardContent,
CardFooter,
Form,
FormControl,
FormField,
Input_Shadcn_,
Switch,
} from 'ui'
import { PageSection, PageSectionContent } from 'ui-patterns'
import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'
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 { useOAuthServerAppsQuery } from '@/data/oauth-server-apps/oauth-server-apps-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { DOCS_URL } from '@/lib/constants'
const OAuthEndpointsTable = dynamic(() =>
import('./OAuthEndpointsTable').then((mod) => ({ default: mod.OAuthEndpointsTable }))
)
const configUrlSchema = z.object({
id: z.string(),
name: z.string(),
value: z.string(),
description: z.string().optional(),
})
const schema = z
.object({
OAUTH_SERVER_ENABLED: z.boolean().default(false),
OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: z.boolean().default(false),
OAUTH_SERVER_AUTHORIZATION_PATH: z.string().default(''),
availableScopes: z.array(z.string()).default(['openid', 'email', 'profile']),
config_urls: z.array(configUrlSchema).optional(),
})
.superRefine((data, ctx) => {
if (data.OAUTH_SERVER_ENABLED && data.OAUTH_SERVER_AUTHORIZATION_PATH.trim() === '') {
ctx.addIssue({
path: ['OAUTH_SERVER_AUTHORIZATION_PATH'],
code: z.ZodIssueCode.custom,
message: 'Authorization Path is required when OAuth Server is enabled.',
})
}
})
interface ConfigUrl {
id: string
name: string
value: string
description?: string
}
interface OAuthServerSettings {
OAUTH_SERVER_ENABLED: boolean
OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: boolean
OAUTH_SERVER_AUTHORIZATION_PATH?: string
availableScopes: string[]
config_urls?: ConfigUrl[]
}
export const OAuthServerSettingsForm = () => {
const { ref: projectRef } = useParams()
const {
data: authConfig,
isPending: isAuthConfigLoading,
isSuccess,
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isPending } = useAuthConfigUpdateMutation({
onSuccess: (_, variables) => {
toast.success('OAuth server settings updated successfully')
form.reset({
OAUTH_SERVER_ENABLED: variables.config.OAUTH_SERVER_ENABLED ?? false,
OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION:
variables.config.OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION ?? false,
OAUTH_SERVER_AUTHORIZATION_PATH:
variables.config.OAUTH_SERVER_AUTHORIZATION_PATH ?? '/oauth/consent',
availableScopes: ['openid', 'email', 'profile'],
})
},
onError: (error) => {
toast.error(`Failed to update OAuth server settings: ${error?.message}`)
},
})
const [showDynamicAppsConfirmation, setShowDynamicAppsConfirmation] = useState(false)
const [showDisableOAuthServerConfirmation, setShowDisableOAuthServerConfirmation] =
useState(false)
const {
can: canReadConfig,
isLoading: isLoadingPermissions,
isSuccess: isPermissionsLoaded,
} = useAsyncCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const { data: oAuthAppsData } = useOAuthServerAppsQuery({ projectRef })
const oauthApps = oAuthAppsData?.clients || []
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const form = useForm<OAuthServerSettings>({
resolver: zodResolver(schema),
defaultValues: {
OAUTH_SERVER_ENABLED: true,
OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: false,
OAUTH_SERVER_AUTHORIZATION_PATH: '/oauth/consent',
availableScopes: ['openid', 'email', 'profile'],
},
})
// Reset the values when the authConfig is loaded
useEffect(() => {
if (isSuccess && authConfig) {
form.reset({
OAUTH_SERVER_ENABLED: authConfig.OAUTH_SERVER_ENABLED ?? false,
OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION:
authConfig.OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION ?? false,
OAUTH_SERVER_AUTHORIZATION_PATH:
authConfig.OAUTH_SERVER_AUTHORIZATION_PATH ?? '/oauth/consent',
availableScopes: ['openid', 'email', 'profile'], // Keep default scopes
})
}
}, [isSuccess])
const onSubmit = async (values: OAuthServerSettings) => {
if (!projectRef) return console.error('Project ref is required')
const config = {
OAUTH_SERVER_ENABLED: values.OAUTH_SERVER_ENABLED,
OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: values.OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION,
OAUTH_SERVER_AUTHORIZATION_PATH: values.OAUTH_SERVER_AUTHORIZATION_PATH,
}
updateAuthConfig({ projectRef, config })
}
const handleDynamicAppsToggle = (checked: boolean) => {
if (checked) {
setShowDynamicAppsConfirmation(true)
} else {
form.setValue('OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION', false, { shouldDirty: true })
}
}
const confirmDynamicApps = () => {
form.setValue('OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION', true, { shouldDirty: true })
setShowDynamicAppsConfirmation(false)
}
const cancelDynamicApps = () => {
setShowDynamicAppsConfirmation(false)
}
const handleOAuthServerToggle = (checked: boolean) => {
if (!checked && oauthApps.length > 0) {
setShowDisableOAuthServerConfirmation(true)
} else {
form.setValue('OAUTH_SERVER_ENABLED', checked, { shouldDirty: true })
}
}
const confirmDisableOAuthServer = () => {
form.setValue('OAUTH_SERVER_ENABLED', false, { shouldDirty: true })
setShowDisableOAuthServerConfirmation(false)
}
const cancelDisableOAuthServer = () => {
setShowDisableOAuthServerConfirmation(false)
}
if (isPermissionsLoaded && !canReadConfig) {
return <NoPermission resourceText="view OAuth server settings" />
}
if (isAuthConfigLoading || isLoadingPermissions) {
return (
<PageSection>
<PageSectionContent>
<Card>
<CardContent>
<GenericSkeletonLoader />
</CardContent>
</Card>
<OAuthEndpointsTable isLoading />
</PageSectionContent>
</PageSection>
)
}
return (
<>
<PageSection>
<PageSectionContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardContent>
<FormField
control={form.control}
name="OAUTH_SERVER_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enable the Supabase OAuth Server"
description="Enable OAuth server functionality for your project to create and manage OAuth applications."
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={handleOAuthServerToggle}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
{form.watch('OAUTH_SERVER_ENABLED') && (
<>
<CardContent>
<FormItemLayout
label="Site URL"
layout="flex-row-reverse"
description={
<>
The base URL of your application, configured in{' '}
<Link
href={`/project/${projectRef}/auth/url-configuration`}
rel="noreferrer"
className="text-foreground-light underline hover:text-foreground transition"
>
Auth URL Configuration
</Link>{' '}
settings.
</>
}
>
<Input_Shadcn_
value={authConfig?.SITE_URL}
disabled
placeholder="https://example.com"
/>
</FormItemLayout>
</CardContent>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="OAUTH_SERVER_AUTHORIZATION_PATH"
render={({ field }) => (
<FormItemLayout
label="Authorization Path"
layout="flex-row-reverse"
description="Path where you'll implement the OAuth authorization UI (consent screens)."
>
<FormControl>
<Input_Shadcn_ {...field} placeholder="/auth/authorize" />
</FormControl>
</FormItemLayout>
)}
/>
{(() => {
const siteUrl = authConfig?.SITE_URL?.trim()
const authorizationPath =
form.watch('OAUTH_SERVER_AUTHORIZATION_PATH')?.trim() || '/oauth/consent'
const authorizationUrl = siteUrl ? `${siteUrl}${authorizationPath}` : ''
return (
<Admonition
type="tip"
title="Make sure this path is implemented in your application."
description={
<>
Preview Authorization URL:{' '}
{authorizationUrl ? (
<a
href={authorizationUrl}
target="_blank"
rel="noreferrer"
className="text-foreground-light underline hover:text-foreground transition"
>
{authorizationUrl}
</a>
) : (
<span className="text-foreground-light">
Set a Site URL to preview
</span>
)}
</>
}
/>
)
})()}
</CardContent>
<CardContent>
<FormField
control={form.control}
name="OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow Dynamic OAuth Apps"
description={
<>
Enable dynamic OAuth app registration. Apps can be registered
programmatically via APIs.{' '}
<InlineLink
href={`${DOCS_URL}/guides/auth/oauth-server/mcp-authentication#oauth-client-setup`}
>
Learn more
</InlineLink>
</>
}
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={handleDynamicAppsToggle}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
</>
)}
<CardFooter className="justify-end space-x-2">
<Button type="default" onClick={() => form.reset()} disabled={isPending}>
Cancel
</Button>
<Button
type="primary"
htmlType="submit"
disabled={!canUpdateConfig || !form.formState.isDirty}
loading={isPending}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
{isSuccess && authConfig?.OAUTH_SERVER_ENABLED && form.watch('OAUTH_SERVER_ENABLED') && (
<OAuthEndpointsTable isLoading={isPending} />
)}
{/* Dynamic Apps Confirmation Modal */}
<ConfirmationModal
variant="warning"
visible={showDynamicAppsConfirmation}
size="large"
title="Enable dynamic OAuth app registration"
confirmLabel="Enable dynamic app registration"
onConfirm={confirmDynamicApps}
onCancel={cancelDynamicApps}
alert={{
title:
'By confirming, you acknowledge the risks and would like to move forward with enabling dynamic OAuth app registration.',
}}
>
<p className="text-sm text-foreground-lighter pb-4">
Dynamic OAuth apps (also known as dynamic client registration) exposes a public endpoint
allowing anyone to register OAuth clients. Bad actors could create malicious apps with
legitimate-sounding names to phish your users for authorization.
</p>
<p className="text-sm text-foreground-lighter pb-4">
You may also see spam registrations that are difficult to trace or moderate, making it
harder to identify trustworthy applications in your OAuth apps list.
</p>
<p className="text-sm text-foreground-lighter pb-4">
Only enable this if you have a specific use case requiring programmatic client
registration and understand the security implications.
</p>
</ConfirmationModal>
{/* Disable OAuth Server Confirmation Modal */}
<ConfirmationModal
variant="warning"
visible={showDisableOAuthServerConfirmation}
size="large"
title="Disable OAuth Server"
confirmLabel="Disable OAuth Server"
onConfirm={confirmDisableOAuthServer}
onCancel={cancelDisableOAuthServer}
alert={{
title: `You have ${oauthApps.length} active OAuth app${oauthApps.length > 1 ? 's' : ''} that will be deactivated.`,
}}
>
<p className="text-sm text-foreground-lighter pb-4">
Disabling the OAuth Server will immediately deactivate all OAuth applications and prevent
new authentication flows from working. This action will affect all users currently using
your OAuth applications.
</p>
<p className="text-sm text-foreground-lighter pb-4">
<strong>What will happen:</strong>
</p>
<ul className="text-sm text-foreground-lighter pb-4 list-disc list-inside space-y-1">
<li>All OAuth apps will be deactivated</li>
<li>Existing access tokens will become invalid</li>
<li>Users won't be able to sign in through OAuth flows</li>
<li>Third-party integrations will stop working</li>
</ul>
<p className="text-sm text-foreground-lighter pb-4">
You can re-enable the OAuth Server at any time.
</p>
</ConfirmationModal>
</>
)
}