CreateOrUpdateOAuthAppSheet() — supabase Function Reference
Architecture documentation for the CreateOrUpdateOAuthAppSheet() function in CreateOrUpdateOAuthAppSheet.tsx from the supabase codebase.
Entity Profile
Relationship Graph
Source Code
apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx lines 83–521
export const CreateOrUpdateOAuthAppSheet = ({
visible,
appToEdit,
onSuccess,
onCancel,
}: CreateOrUpdateOAuthAppSheetProps) => {
const { ref: projectRef } = useParams()
const isEditMode = !!appToEdit
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false)
const uploadButtonRef = useRef<HTMLInputElement>(null)
const [logoFile, setLogoFile] = useState<File>()
const [logoUrl, setLogoUrl] = useState<string>()
const [logoRemoved, setLogoRemoved] = useState(false)
const hasLogo = logoUrl !== undefined
const isPublicClient = appToEdit?.client_type === 'public'
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: initialValues,
})
const {
fields: redirectUriFields,
append: appendRedirectUri,
remove: removeRedirectUri,
} = useFieldArray({
name: 'redirect_uris',
control: form.control,
})
const { data: endpointData } = useProjectEndpointQuery({ projectRef })
const { mutate: createOAuthApp, isPending: isCreating } = useOAuthServerAppCreateMutation({
onSuccess: (data) => {
toast.success(`Successfully created OAuth app "${data.client_name}"`)
onSuccess(data)
},
})
const { mutate: updateOAuthApp, isPending: isUpdating } = useOAuthServerAppUpdateMutation({
onSuccess: (data) => {
toast.success(`Successfully updated OAuth app "${data.client_name}"`)
onSuccess(data)
},
})
const { mutate: regenerateSecret, isPending: isRegenerating } =
useOAuthServerAppRegenerateSecretMutation({
onSuccess: (data) => {
if (data) {
toast.success(`Successfully regenerated client secret for "${appToEdit?.client_name}"`)
onSuccess(data)
setShowRegenerateDialog(false)
}
},
})
useEffect(() => {
if (visible) {
setLogoFile(undefined)
setLogoRemoved(false)
if (appToEdit) {
form.reset({
name: appToEdit.client_name,
type: 'manual' as const,
redirect_uris:
appToEdit.redirect_uris && appToEdit.redirect_uris.length > 0
? appToEdit.redirect_uris.map((uri) => ({ value: uri }))
: [{ value: '' }],
client_type: appToEdit.client_type,
client_id: appToEdit.client_id,
client_secret: '****************************************************************',
logo_uri: appToEdit.logo_uri || undefined,
})
setLogoUrl(appToEdit.logo_uri || undefined)
} else {
form.reset(initialValues)
setLogoUrl(undefined)
}
}
}, [visible, appToEdit, form])
const onFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
event.persist()
const files = event.target.files
if (files && files.length > 0) {
const file = files[0]
setLogoFile(file)
setLogoUrl(URL.createObjectURL(file))
setLogoRemoved(false)
event.target.value = ''
}
}
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
const validRedirectUris = data.redirect_uris
.map((uri) => uri.value.trim())
.filter((uri) => uri !== '')
let uploadedLogoUri: string | undefined = undefined
if (logoRemoved) {
uploadedLogoUri = ''
} else if (logoFile) {
const reader = new FileReader()
uploadedLogoUri = await new Promise<string>((resolve) => {
reader.onloadend = () => resolve(reader.result as string)
reader.readAsDataURL(logoFile)
})
} else if (logoUrl) {
uploadedLogoUri = logoUrl
}
if (isEditMode && appToEdit) {
const payload: UpdateOAuthClientParams = {
client_name: data.name,
redirect_uris: validRedirectUris,
logo_uri: uploadedLogoUri,
}
updateOAuthApp({
projectRef,
clientId: appToEdit.client_id,
clientEndpoint: endpointData?.endpoint,
...payload,
})
} else {
const payload: CreateOAuthClientParams & { logo_uri?: string; client_type?: string } = {
client_name: data.name,
client_uri: '',
client_type: data.client_type,
redirect_uris: validRedirectUris,
logo_uri: uploadedLogoUri || undefined,
}
createOAuthApp({
projectRef,
clientEndpoint: endpointData?.endpoint,
...payload,
})
}
}
const onClose = () => {
form.reset(initialValues)
onCancel()
}
const handleRegenerateSecret = () => {
setShowRegenerateDialog(true)
}
const handleConfirmRegenerate = () => {
regenerateSecret({
projectRef,
clientId: appToEdit?.client_id,
clientEndpoint: endpointData?.endpoint,
})
}
const handleUploadLogo = () => uploadButtonRef.current?.click()
const handleRemoveLogo = () => {
setLogoFile(undefined)
setLogoUrl(undefined)
setLogoRemoved(true)
}
return (
<>
<Sheet open={visible} onOpenChange={() => onCancel()}>
<SheetContent
size="lg"
showClose={false}
className="flex flex-col gap-0"
tabIndex={undefined}
>
<SheetHeader>
<div className="flex flex-row gap-3 items-center">
<SheetClose
className={cn(
'text-muted hover:text ring-offset-background transition-opacity hover:opacity-100',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'disabled:pointer-events-none data-[state=open]:bg-secondary',
'transition'
)}
>
<X className="h-3 w-3" />
<span className="sr-only">Close</span>
</SheetClose>
<SheetTitle className="truncate">
{isEditMode ? 'Update OAuth app' : 'Create a new OAuth app'}
</SheetTitle>
</div>
</SheetHeader>
<SheetSection className="overflow-auto flex-grow px-0">
<Form_Shadcn_ {...form}>
<form className="space-y-6" onSubmit={form.handleSubmit(onSubmit)} id={FORM_ID}>
<div className="px-5 flex items-start justify-between gap-4">
<div className="flex-grow space-y-4">
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout label="Name">
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} placeholder="My OAuth App" />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
control={form.control}
name="logo_uri"
render={() => (
<FormItemLayout label="Logo" description="Upload a logo for your OAuth app">
<FormControl_Shadcn_>
<div className="flex gap-4 items-center">
<button
type="button"
onClick={handleUploadLogo}
className={cn(
'flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong'
)}
style={{
backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none',
}}
>
{!hasLogo && <Upload size={14} />}
</button>
<div className="flex gap-2 items-center">
<Button
type="default"
size="tiny"
icon={<Upload size={14} />}
onClick={handleUploadLogo}
>
Upload
</Button>
{hasLogo && (
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
onClick={handleRemoveLogo}
/>
)}
</div>
<input
type="file"
ref={uploadButtonRef}
className="hidden"
accept="image/png, image/jpeg"
onChange={onFileUpload}
/>
</div>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
</div>
{isEditMode && appToEdit && (
<>
<Separator />
<div className="px-5">
<Panel>
<Panel.Content className="space-y-2">
<FormField_Shadcn_
control={form.control}
name="client_id"
render={() => (
<FormItemLayout label="Client ID">
<FormControl_Shadcn_>
<Input
copy
readOnly
className="input-mono"
value={appToEdit.client_id}
onChange={() => {}}
onCopy={() => toast.success('Client ID copied to clipboard')}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{!isPublicClient && (
<>
<FormField_Shadcn_
control={form.control}
name="client_secret"
render={() => (
<FormItemLayout
label="Client Secret"
description="Client secret is hidden for security. Use the regenerate button to create a new one."
>
<FormControl_Shadcn_>
<Input
readOnly
type="password"
className="input-mono"
value="****************************************************************"
onChange={() => {}}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<Button
type="default"
onClick={handleRegenerateSecret}
className="w-full"
disabled={isRegenerating}
>
Regenerate Client Secret
</Button>
</>
)}
</Panel.Content>
</Panel>
</div>
</>
)}
<div className="px-5 gap-2 flex flex-col">
<FormLabel_Shadcn_ className="text-foreground">Redirect URIs</FormLabel_Shadcn_>
<div className="space-y-2">
{redirectUriFields.map((fieldItem, index) => (
<FormField_Shadcn_
control={form.control}
key={fieldItem.id}
name={`redirect_uris.${index}.value`}
render={({ field: inputField }) => (
<FormItem_Shadcn_>
<div className="flex flex-row gap-2">
<FormControl_Shadcn_>
<Input_Shadcn_
{...inputField}
placeholder={'https://example.com/callback'}
onChange={(e) => {
inputField.onChange(e)
}}
/>
</FormControl_Shadcn_>
{redirectUriFields.length > 1 && (
<Button
type="default"
size="tiny"
className="h-[34px]"
icon={<Trash2 size={12} />}
onClick={() => removeRedirectUri(index)}
/>
)}
</div>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
)}
/>
))}
</div>
<div>
<Button
type="default"
icon={<Plus strokeWidth={1.5} />}
onClick={() => appendRedirectUri({ value: '' })}
>
Add redirect URI
</Button>
</div>
<FormDescription_Shadcn_ className="text-foreground-lighter">
URLs where users will be redirected after authentication.
</FormDescription_Shadcn_>
</div>
<Separator />
<FormField_Shadcn_
control={form.control}
name="client_type"
render={({ field }) => (
<FormItemLayout
label="Public Client"
layout="flex"
description={
<>
If enabled, the Authorization Code with PKCE (Proof Key for Code Exchange)
flow can be used, particularly beneficial for applications that cannot
securely store Client Secrets, such as native and mobile apps. This cannot
be changed after creation.{' '}
<InlineLink href={`${DOCS_URL}/guides/auth/oauth/public-oauth-apps`}>
Learn more
</InlineLink>
</>
}
className={'px-5'}
>
<FormControl_Shadcn_>
<Switch
checked={field.value === 'public'}
onCheckedChange={(checked) =>
field.onChange(checked ? 'public' : 'confidential')
}
disabled={isEditMode}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</form>
</Form_Shadcn_>
</SheetSection>
<SheetFooter>
<Button type="default" disabled={isCreating || isUpdating} onClick={onClose}>
Cancel
</Button>
<Button htmlType="submit" form={FORM_ID} loading={isCreating || isUpdating}>
{isEditMode ? 'Update app' : 'Create app'}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<ConfirmationModal
variant="warning"
visible={showRegenerateDialog}
loading={isRegenerating}
title="Confirm regenerating client secret"
confirmLabel="Confirm"
onCancel={() => setShowRegenerateDialog(false)}
onConfirm={handleConfirmRegenerate}
>
<p className="text-sm text-foreground-light">
Are you sure you wish to regenerate the client secret for "{appToEdit?.client_name}"?
You'll need to update it in all applications that use it. This action cannot be undone.
</p>
</ConfirmationModal>
</>
)
}
Domain
Subdomains
Source
Analyze Your Own Codebase
Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.
Try Supermodel Free