Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ import type {
OAuthClient,
UpdateOAuthClientParams,
} from '@supabase/supabase-js'
import { Plus, Trash2, Upload, X } from 'lucide-react'
import { type ChangeEvent, useEffect, useRef, useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'

import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import Panel from 'components/ui/Panel'
Expand All @@ -18,15 +12,20 @@ import { useOAuthServerAppCreateMutation } from 'data/oauth-server-apps/oauth-se
import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation'
import { useOAuthServerAppUpdateMutation } from 'data/oauth-server-apps/oauth-server-app-update-mutation'
import { DOCS_URL } from 'lib/constants'
import { Plus, Trash2, Upload, X } from 'lucide-react'
import { useEffect, useRef, useState, type ChangeEvent } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
cn,
Form_Shadcn_,
FormControl_Shadcn_,
FormDescription_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormLabel_Shadcn_,
FormMessage_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
Separator,
Sheet,
Expand All @@ -37,11 +36,11 @@ import {
SheetSection,
SheetTitle,
Switch,
cn,
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'

interface CreateOrUpdateOAuthAppSheetProps {
visible: boolean
Expand Down Expand Up @@ -87,12 +86,14 @@ export const CreateOrUpdateOAuthAppSheet = ({
onCancel,
}: CreateOrUpdateOAuthAppSheetProps) => {
const { ref: projectRef } = useParams()
const isEditMode = !!appToEdit
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false)
const uploadButtonRef = useRef<HTMLInputElement>(null)

const [showRegenerateDialog, setShowRegenerateDialog] = useState(false)
const [logoFile, setLogoFile] = useState<File>()
const [logoUrl, setLogoUrl] = useState<string>()
const [logoRemoved, setLogoRemoved] = useState(false)

const isEditMode = !!appToEdit
const hasLogo = logoUrl !== undefined
const isPublicClient = appToEdit?.client_type === 'public'

Expand Down Expand Up @@ -391,10 +392,10 @@ export const CreateOrUpdateOAuthAppSheet = ({
<Button
type="default"
onClick={handleRegenerateSecret}
className="w-full"
className="w-min"
disabled={isRegenerating}
>
Regenerate Client Secret
Regenerate client secret
</Button>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { OAuthClient } from '@supabase/supabase-js'
import { useParams } from 'common'

import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import type { OAuthServerAppDeleteVariables } from 'data/oauth-server-apps/oauth-server-app-delete-mutation'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
Expand Down Expand Up @@ -39,7 +38,8 @@ export const DeleteOAuthAppModal = ({
visible={visible}
title={
<>
Confirm to delete OAuth app <code className="text-sm">{selectedApp?.client_name}</code>
Confirm to delete OAuth app{' '}
<code className="text-code-inline">{selectedApp?.client_name}</code>
</>
}
confirmLabel="Confirm delete"
Expand Down
139 changes: 73 additions & 66 deletions apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import type { OAuthClient } from '@supabase/supabase-js'
import { Edit, MoreVertical, Plus, RotateCw, Search, Trash, X } from 'lucide-react'
import Link from 'next/link'
import { parseAsBoolean, parseAsStringLiteral, useQueryState } from 'nuqs'
import { useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'

import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
Expand All @@ -14,14 +8,18 @@ import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import { useOAuthServerAppDeleteMutation } from 'data/oauth-server-apps/oauth-server-app-delete-mutation'
import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation'
import { useOAuthServerAppsQuery } from 'data/oauth-server-apps/oauth-server-apps-query'
import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
import { Edit, MoreVertical, Plus, RotateCw, Search, Trash, X } from 'lucide-react'
import Link from 'next/link'
import { parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
Badge,
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
Table,
Expand All @@ -36,6 +34,7 @@ import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'

import { CreateOrUpdateOAuthAppSheet } from './CreateOrUpdateOAuthAppSheet'
import { DeleteOAuthAppModal } from './DeleteOAuthAppModal'
import { NewOAuthAppBanner } from './NewOAuthAppBanner'
Expand All @@ -62,77 +61,64 @@ type OAuthAppsSortOrder = OAuthAppsSort extends `${string}:${infer Order}` ? Ord

export const OAuthAppsList = () => {
const { ref: projectRef } = useParams()
const { data: authConfig, isPending: isAuthConfigLoading } = useAuthConfigQuery({ projectRef })
const {
data: authConfig,
isPending: isAuthConfigLoading,
isSuccess: isSuccessAuthConfig,
} = useAuthConfigQuery({ projectRef })
const isOAuthServerEnabled = !!authConfig?.OAUTH_SERVER_ENABLED

const [newOAuthApp, setNewOAuthApp] = useState<OAuthClient | undefined>(undefined)
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false)
const [selectedApp, setSelectedApp] = useState<OAuthClient>()
const [filteredRegistrationTypes, setFilteredRegistrationTypes] = useState<string[]>([])
const [filteredClientTypes, setFilteredClientTypes] = useState<string[]>([])
const deletingOAuthAppIdRef = useRef<string | null>(null)
const [filterString, setFilterString] = useState<string>('')

const { data, isPending: isLoading, isError, error } = useOAuthServerAppsQuery({ projectRef })
const { data: endpointData } = useProjectEndpointQuery({ projectRef })
const {
data,
error,
isPending: isLoading,
isSuccess,
isError,
} = useOAuthServerAppsQuery({ projectRef })
const oAuthApps = useMemo(() => data?.clients || [], [data])

const { mutateAsync: regenerateSecret, isPending: isRegenerating } =
useOAuthServerAppRegenerateSecretMutation({
onSuccess: (data) => {
if (data) {
setNewOAuthApp(data)
}
if (data) setNewOAuthApp(data)
},
})

const { data: endpointData } = useProjectEndpointQuery({ projectRef })

const oAuthApps = useMemo(() => data?.clients || [], [data])
const [sort, setSort] = useQueryState(
'sort',
parseAsStringLiteral<OAuthAppsSort>(OAUTH_APPS_SORT_VALUES).withDefault('name:asc')
)

const [showCreateSheet, setShowCreateSheet] = useQueryState(
'new',
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
parseAsBoolean.withDefault(false)
)

// Prevent opening the create sheet if OAuth Server is disabled
useEffect(() => {
if (!isOAuthServerEnabled && showCreateSheet) {
setShowCreateSheet(false)
}
}, [isOAuthServerEnabled, showCreateSheet, setShowCreateSheet])
const [selectedAppToEdit, setSelectedAppToEdit] = useQueryState('edit', parseAsString)
const appToEdit = oAuthApps?.find((app) => app.client_id === selectedAppToEdit)

const { setValue: setSelectedAppToEdit, value: appToEdit } = useQueryStateWithSelect({
urlKey: 'edit',
select: (client_id: string) =>
client_id ? oAuthApps?.find((app) => app.client_id === client_id) : undefined,
enabled: !!oAuthApps?.length,
onError: (_error, selectedId) =>
handleErrorOnDelete(deletingOAuthAppIdRef, selectedId, `OAuth App not found`),
})
const [selectedAppToDelete, setSelectedAppToDelete] = useQueryState('delete', parseAsString)
const appToDelete = oAuthApps?.find((app) => app.client_id === selectedAppToDelete)

const { setValue: setSelectedAppToDelete, value: appToDelete } = useQueryStateWithSelect({
urlKey: 'delete',
select: (client_id: string) =>
client_id ? oAuthApps?.find((app) => app.client_id === client_id) : undefined,
enabled: !!oAuthApps?.length,
onError: (_error, selectedId) =>
handleErrorOnDelete(deletingOAuthAppIdRef, selectedId, `OAuth App not found`),
})

const { mutate: deleteOAuthApp, isPending: isDeletingApp } = useOAuthServerAppDeleteMutation({
const {
mutate: deleteOAuthApp,
isPending: isDeletingApp,
isSuccess: isSuccessDelete,
} = useOAuthServerAppDeleteMutation({
onSuccess: () => {
toast.success(`Successfully deleted OAuth app`)
setSelectedAppToDelete(null)
},
onError: () => {
deletingOAuthAppIdRef.current = null
},
})

const [filterString, setFilterString] = useState<string>('')

const [sort, setSort] = useQueryState(
'sort',
parseAsStringLiteral<OAuthAppsSort>(OAUTH_APPS_SORT_VALUES).withDefault('name:asc')
)

const filteredAndSortedOAuthApps = useMemo(() => {
const filtered = filterOAuthApps({
apps: oAuthApps,
Expand Down Expand Up @@ -194,6 +180,27 @@ export const OAuthAppsList = () => {
const isEditMode = !!appToEdit
const isCreateOrUpdateSheetVisible = isCreateMode || isEditMode

// Prevent opening the create sheet if OAuth Server is disabled
useEffect(() => {
if (isSuccessAuthConfig && !isOAuthServerEnabled && showCreateSheet) {
setShowCreateSheet(false)
}
}, [isSuccessAuthConfig, isOAuthServerEnabled, showCreateSheet, setShowCreateSheet])

useEffect(() => {
if (isSuccess && !!selectedAppToEdit && !appToEdit) {
toast('App not found')
setSelectedAppToEdit(null)
}
}, [appToEdit, isSuccess, selectedAppToEdit, setSelectedAppToEdit])

useEffect(() => {
if (isSuccess && !!selectedAppToDelete && !appToDelete && !isSuccessDelete) {
toast('App not found')
setSelectedAppToDelete(null)
}
}, [appToDelete, isSuccess, isSuccessDelete, selectedAppToDelete, setSelectedAppToDelete])

if (isAuthConfigLoading || (isOAuthServerEnabled && isLoading)) {
return <GenericSkeletonLoader />
}
Expand Down Expand Up @@ -291,7 +298,7 @@ export const OAuthAppsList = () => {
<Table containerProps={{ stickyLastColumn: true }}>
<TableHeader>
<TableRow>
<TableHead>
<TableHead className="w-48 max-w-48 flex">
<TableHeadSort column="name" currentSort={sort} onSortChange={handleSortChange}>
Name
</TableHeadSort>
Expand Down Expand Up @@ -340,7 +347,7 @@ export const OAuthAppsList = () => {
{filteredAndSortedOAuthApps.length > 0 &&
filteredAndSortedOAuthApps.map((app) => (
<TableRow key={app.client_id} className="w-full">
<TableCell className="w-48 max-w-48 flex" title={app.client_name}>
<TableCell title={app.client_name}>
<Button
type="text"
className="text-link-table-cell text-sm p-0 hover:bg-transparent title [&>span]:!w-full"
Expand All @@ -351,16 +358,16 @@ export const OAuthAppsList = () => {
</Button>
</TableCell>
<TableCell title={app.client_id}>
<Badge className="font-mono">{app.client_id}</Badge>
</TableCell>
<TableCell className="text-xs text-foreground-light max-w-28 capitalize">
{app.client_type}
</TableCell>
<TableCell className="text-xs text-foreground-light max-w-28 capitalize">
{app.registration_type}
<code className="text-code-inline">{app.client_id}</code>
</TableCell>
<TableCell className="text-xs text-foreground-light min-w-28 max-w-40 w-1/6">
<TimestampInfo utcTimestamp={app.created_at} labelFormat="D MMM, YYYY" />
<TableCell className="max-w-28 capitalize">{app.client_type}</TableCell>
<TableCell className="max-w-28 capitalize">{app.registration_type}</TableCell>
<TableCell className="min-w-28 max-w-40 w-1/6">
<TimestampInfo
className="text-sm"
utcTimestamp={app.created_at}
labelFormat="D MMM, YYYY"
/>
</TableCell>
<TableCell className="max-w-20 bg-surface-100 @[944px]:hover:bg-surface-200 px-6">
<div className="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center border-l @[944px]:border-l-0">
Expand All @@ -376,7 +383,7 @@ export const OAuthAppsList = () => {
}}
>
<Edit size={12} />
<p>Update</p>
<p>Edit OAuth app</p>
</DropdownMenuItem>
{app.client_type === 'confidential' && (
<DropdownMenuItem
Expand All @@ -390,12 +397,13 @@ export const OAuthAppsList = () => {
<p>Regenerate client secret</p>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
className="space-x-2"
onClick={() => setSelectedAppToDelete(app.client_id)}
>
<Trash size={12} />
<p>Delete</p>
<p>Delete OAuth app</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down Expand Up @@ -434,7 +442,6 @@ export const OAuthAppsList = () => {
selectedApp={appToDelete}
setVisible={setSelectedAppToDelete}
onDelete={(params: Parameters<typeof deleteOAuthApp>[0]) => {
deletingOAuthAppIdRef.current = params.clientId ?? null
deleteOAuthApp(params)
}}
isLoading={isDeletingApp}
Expand Down
Loading
Loading