diff --git a/bun.lock b/bun.lock index fa7c6d1d2..6847d990e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@e2b/dashboard", @@ -25,6 +26,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-portal": "^1.1.9", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-select": "^2.1.7", diff --git a/package.json b/package.json index 04e2768df..7d275f556 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-portal": "^1.1.9", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-select": "^2.1.7", diff --git a/src/core/server/auth/ory/oauth-session.ts b/src/core/server/auth/ory/oauth-session.ts index a3c382d77..6388b6733 100644 --- a/src/core/server/auth/ory/oauth-session.ts +++ b/src/core/server/auth/ory/oauth-session.ts @@ -18,8 +18,12 @@ export async function revokeOryOAuthSessionsForSubject( return } - await revokeConsentSessions(subject, clientId) - await revokeLoginSessions(subject) + // Independent Hydra revocations; run concurrently. Each call logs and + // swallows its own errors. + await Promise.all([ + revokeConsentSessions(subject, clientId), + revokeLoginSessions(subject), + ]) } async function revokeConsentSessions( diff --git a/src/core/server/auth/ory/signout-flow.ts b/src/core/server/auth/ory/signout-flow.ts index dc9ff9a8d..cf08004f3 100644 --- a/src/core/server/auth/ory/signout-flow.ts +++ b/src/core/server/auth/ory/signout-flow.ts @@ -44,13 +44,14 @@ export async function completeOrySignOut(origin = BASE_URL): Promise { ) } - if (userId) { - await revokeOryOAuthSessionsForSubject(userId) - } - - if (identityId) { - await revokeKratosSessionsForIdentity(identityId) - } + // Hydra OAuth and Kratos session revocations are independent admin calls; + // run them concurrently to keep the sign-out action fast. Both helpers + // log-and-swallow their own errors, and the Kratos helper retries 429 + // contention, so Promise.all never rejects here. + await Promise.all([ + userId ? revokeOryOAuthSessionsForSubject(userId) : null, + identityId ? revokeKratosSessionsForIdentity(identityId) : null, + ]) const logoutUrl = idToken ? buildOryLogoutUrl({ idToken, origin }) : null return (logoutUrl ?? postLogoutUrl).toString() diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index cd844f3bb..eb642bb61 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -1,5 +1,6 @@ 'use client' +import { Portal } from '@radix-ui/react-portal' import Link from 'next/link' import { useState } from 'react' import { PROTECTED_URLS } from '@/configs/urls' @@ -20,6 +21,7 @@ import { LogoutIcon, UnpackIcon, } from '@/ui/primitives/icons' +import { Loader } from '@/ui/primitives/loader' import { SidebarMenuButton, SidebarMenuItem } from '@/ui/primitives/sidebar' import { useDashboard } from '../context' import { CreateTeamDialog } from './create-team-dialog' @@ -29,9 +31,17 @@ import { TeamAvatar } from './team-avatar' export default function DashboardSidebarMenu() { const { team } = useDashboard() const [createTeamOpen, setCreateTeamOpen] = useState(false) + // explicit state instead of useTransition: a sync transition callback + // settles immediately, so isPending would flip back to false while the + // sign-out action is still in flight; this stays true until the redirect + // navigates away, and only resets if the action fails before that + const [isLoggingOut, setIsLoggingOut] = useState(false) const handleLogout = () => { - signOutAction() + setIsLoggingOut(true) + signOutAction().catch(() => { + setIsLoggingOut(false) + }) } return ( @@ -94,6 +104,7 @@ export default function DashboardSidebarMenu() { Log out @@ -106,6 +117,12 @@ export default function DashboardSidebarMenu() { open={createTeamOpen} onOpenChange={setCreateTeamOpen} /> + {isLoggingOut && ( + + + Logging out... + + )} ) }