import { zodResolver } from '@hookform/resolvers/zod' import type { OAuthScope } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { Edit, Upload } from 'lucide-react' import { ChangeEvent, useEffect, useRef, useState } from 'react' import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Badge, Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Form, FormControl, FormField, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, Modal, SidePanel, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' import { AuthorizeRequesterDetails } from '../AuthorizeRequesterDetails' import { OAuthSecrets } from '../OAuthSecrets/OAuthSecrets' import { ScopesPanel } from './Scopes' import { DocsButton } from '@/components/ui/DocsButton' import { OAuthAppCreateResponse, useOAuthAppCreateMutation, } from '@/data/oauth/oauth-app-create-mutation' import { useOAuthAppUpdateMutation } from '@/data/oauth/oauth-app-update-mutation' import type { OAuthApp } from '@/data/oauth/oauth-apps-query' import { DOCS_URL } from '@/lib/constants' import { isValidHttpUrl, uuidv4 } from '@/lib/helpers' import { uploadAttachment } from '@/lib/upload' export interface PublishAppSidePanelProps { visible: boolean selectedApp?: OAuthApp onClose: () => void onCreateSuccess: (app: OAuthAppCreateResponse) => void } const formSchema = z.object({ name: z.string().min(1, 'Please provide a name for your application'), website: z .string() .min(1, 'Please provide a URL for your site') .url('Please provide a URL for your site') .refine((value) => isValidHttpUrl(value), 'Please provide a valid URL for your site'), redirect_uris: z .array( z.object({ id: z.string(), value: z.string().min(1, 'Please provide a URL').url('Please provide a URL'), }), { required_error: 'Please provide at least one callback URL' } ) .min(1, 'Please provide at least one callback URL'), }) const getFormDefaultValues = (selectedApp: OAuthApp | undefined) => { if (selectedApp) { return { name: selectedApp.name, website: selectedApp.website, redirect_uris: selectedApp.redirect_uris?.map((url) => { return { id: uuidv4(), value: url } }) ?? [], } } return { name: '', website: '', redirect_uris: [{ id: uuidv4(), value: '' }] } } type FormSchema = z.infer export const PublishAppSidePanel = ({ visible, selectedApp, onClose, onCreateSuccess, }: PublishAppSidePanelProps) => { const { slug } = useParams() const uploadButtonRef = useRef(null) const { mutateAsync: createOAuthApp } = useOAuthAppCreateMutation({ onSuccess: (res, variables) => { toast.success(`Successfully created OAuth app "${variables.name}"!`) onClose() onCreateSuccess(res) }, onError: (error) => { toast.error(`Failed to create OAuth application: ${error.message}`) }, }) const { mutateAsync: updateOAuthApp } = useOAuthAppUpdateMutation({ onSuccess: (_, variables) => { toast.success(`Successfully updated OAuth app "${variables.name}"!`) onClose() }, onError: (error) => { toast.error(`Failed to update OAuth application: ${error.message}`) }, }) const [showPreview, setShowPreview] = useState(false) const [iconFile, setIconFile] = useState() const [iconUrl, setIconUrl] = useState() const [scopes, setScopes] = useState([]) useEffect(() => { if (visible) { setIconFile(undefined) if (selectedApp !== undefined) { setScopes((selectedApp?.scopes ?? []) as OAuthScope[]) setIconUrl(selectedApp.icon === null ? undefined : selectedApp.icon) } else { setScopes([]) setIconUrl(undefined) } } }, [visible, selectedApp]) const onFileUpload = async (event: ChangeEvent) => { event.persist() const [file] = event.target.files || (event as any).dataTransfer.items setIconFile(file) setIconUrl(URL.createObjectURL(file)) event.target.value = '' } const onSubmit: SubmitHandler = async (values) => { if (!slug) return console.error('Slug is required') const { name, website, redirect_uris } = values const uploadedIconUrl = iconFile !== undefined ? await uploadAttachment('oauth-app-icons', `${slug}/${uuidv4()}.png`, iconFile) : iconUrl if (iconFile !== undefined && uploadedIconUrl === undefined) { toast.error('Failed to upload OAuth application icon') return } try { if (selectedApp === undefined) { // Create application await createOAuthApp({ slug, name, website, redirect_uris: redirect_uris.map((uris) => uris.value), scopes, icon: uploadedIconUrl, }) } else { // Update application await updateOAuthApp({ id: selectedApp.id, slug, name, website, redirect_uris: redirect_uris.map((uris) => uris.value), scopes, icon: uploadedIconUrl, }) } } catch { // Error side effects are handled in the mutation hook options } } const form = useForm({ defaultValues: getFormDefaultValues(selectedApp), resolver: zodResolver(formSchema as any), }) const { reset } = form const { errors, isSubmitting } = form.formState useEffect(() => { if (visible) { const defaultValues = getFormDefaultValues(selectedApp) reset(defaultValues) } }, [visible, selectedApp, reset]) const name = useWatch({ name: 'name', control: form.control }) const website = useWatch({ name: 'website', control: form.control }) const { fields: callbackUrlsFields, append: appendCallbackUrl, remove: removeCallbackUrl, } = useFieldArray({ name: 'redirect_uris', control: form.control, }) return ( onClose()} >
( )} /> ( )} />
{iconUrl !== undefined ? (
{ if (uploadButtonRef.current) (uploadButtonRef.current as any).click() }} >

Upload image

{ setIconFile(undefined) setIconUrl(undefined) }} >

Remove image

) : (
{ if (uploadButtonRef.current) (uploadButtonRef.current as any).click() }} >

Upload logo

)}

Authorization callback URLs

All URLs must use HTTPS, except for localhost

{callbackUrlsFields.map((url, index) => ( ( Callback URL} > {callbackUrlsFields.length > 1 ? ( removeCallbackUrl(index)} > Remove ) : null} )} /> ))} {errors.redirect_uris?.root != null ? (

{errors.redirect_uris?.root.message}

) : null}
{selectedApp !== undefined && ( <> )}
Application permissions The application permissions are organized in scopes and will be presented to the user when adding an app to their organization and all of its projects.
setShowPreview(false)} >

Authorize API access for {name}

Preview

Select an organization to grant API access to

Organizations that you have access to will be listed here

This is what your users will see when authorizing with your app

) }