feat(studio): scoped oauth apps, admins can revoke an app

This commit is contained in:
Gildas Garcia committed 2026-10-01 12:28:19 +02:00
1 parent f83aa7f6e5
commit 978e0807dd
2 files changed
+108

No files matched your search

@@ -16,6 +16,7 @@ import {
} from 'ui'
import { OAuthAppsMemberGrantsDialogContent } from './OAuthAppsMemberGrantsDialogContent'
import { OAuthAppsRevokeDialogContent } from './OAuthAppsRevokeDialogContent'
import type { OAuthApprovalItem, OAuthApprovalTarget } from '@/data/oauth-apps/types'
export interface OAuthAppsAuthorizedRowProps {
@@ -78,9 +79,23 @@ export const OAuthAppsAuthorizedRow = ({ approval }: OAuthAppsAuthorizedRowProps
View grants
</DropdownMenuItem>
</DialogTrigger>
<DialogTrigger asChild>
<DropdownMenuItem
className="text-destructive"
onClick={() => setDialogContent('revoke')}
>
Disconnect
</DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
{dialogContent === 'grants' && <OAuthAppsMemberGrantsDialogContent approval={approval} />}
{dialogContent === 'revoke' && (
<OAuthAppsRevokeDialogContent
approval={approval}
onClose={() => setDialogContent(null)}
/>
)}
</Dialog>
</TableCell>
</TableRow>
@@ -0,0 +1,93 @@
import { useParams } from 'common'
import { toast } from 'sonner'
import {
Button,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogTitle,
} from 'ui'
import { useOAuthAppRevokeMutation } from '@/data/oauth-apps/oauth-apps-revoke-mutation'
import type { OAuthApprovalItem } from '@/data/oauth-apps/types'
export interface OAuthAppsRevokeDialogProps {
approval?: OAuthApprovalItem
onClose: () => void
}
export const OAuthAppsRevokeDialogContent = ({ approval, onClose }: OAuthAppsRevokeDialogProps) => {
const { slug } = useParams()
const { mutate: revokeApp, isPending } = useOAuthAppRevokeMutation({
onSuccess: () => {
toast.success(`Revoked access for ${approval?.app.name}`)
onClose()
},
})
if (!approval) return null
const hasOrgGrant = Boolean(approval.org_grant)
return (
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Revoke access for {approval.app.name}</DialogTitle>
</DialogHeader>
<DialogSection className="flex flex-col gap-4 text-sm text-foreground-light">
<p>
This revokes the app at{' '}
<span className="font-medium text-foreground">organization level</span> and affects{' '}
<span className="font-medium text-foreground">all users</span>.
</p>
<div className="flex flex-col gap-2">
<p>Caveats:</p>
<ul className="flex flex-col gap-1">
{hasOrgGrant && (
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>The organization-wide grant will be revoked.</span>
</li>
)}
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>Every member loses access on the apps next request.</span>
</li>
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>Members will need to authorize again to reconnect.</span>
</li>
<li className="flex gap-x-2">
<span aria-hidden>–</span>
<span>
This is <span className="font-medium text-foreground">not</span> a per-user
revocation.
</span>
</li>
</ul>
</div>
</DialogSection>
<DialogFooter>
<DialogClose asChild>
<Button variant="default" disabled={isPending}>
Cancel
</Button>
</DialogClose>
<Button
variant="danger"
loading={isPending}
onClick={() => {
if (approval && slug) revokeApp({ slug, appId: approval.app.id })
}}
>
Revoke
</Button>
</DialogFooter>
</DialogContent>
)
}