From 3b1fac2d6c9c7ca2c7411c1afbe49f1b7665a630 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Sun, 14 Jun 2026 23:06:00 -0700 Subject: [PATCH] feat(web): delete organization from settings danger zone (#1117) --- .../components/settings/settings-content.tsx | 139 +++++++++++++++++- apps/web/hooks/use-delete-organization.ts | 26 ++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 apps/web/hooks/use-delete-organization.ts diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx index 98cb318e..c0f2a167 100644 --- a/apps/web/components/settings/settings-content.tsx +++ b/apps/web/components/settings/settings-content.tsx @@ -13,6 +13,7 @@ import ConnectionsMCP from "@/components/settings/connections-mcp" import Support from "@/components/settings/support" import { ErrorBoundary } from "@/components/error-boundary" import { useRouter } from "next/navigation" +import { useQuery } from "@tanstack/react-query" import { useIsMobile } from "@hooks/use-mobile" import { useLocalStorageUsername } from "@hooks/use-local-storage-username" import { @@ -28,12 +29,14 @@ import { ShieldAlert, ChevronRight, ArrowUpRight, + Building2, } from "lucide-react" import { authClient } from "@lib/auth" import { Dialog, DialogContent, DialogClose } from "@ui/components/dialog" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { useResetOrganization } from "@/hooks/use-reset-organization" import { useDeleteUserAccount } from "@/hooks/use-account-settings" +import { useDeleteOrganization } from "@/hooks/use-delete-organization" import { SettingsOrgSwitcher } from "@/components/settings/settings-org-switcher" export const TABS = [ @@ -129,7 +132,7 @@ export function SettingsContent({ className?: string showIdentity?: boolean }) { - const { user, org } = useAuth() + const { user, org, organizations, setActiveOrg, clearActiveOrg } = useAuth() const router = useRouter() const isMobile = useIsMobile() const localStorageUsername = useLocalStorageUsername() @@ -142,6 +145,25 @@ export function SettingsContent({ const [deleteEmailConfirm, setDeleteEmailConfirm] = useState("") const deleteUserAccount = useDeleteUserAccount() + const [isDeleteOrgDialogOpen, setIsDeleteOrgDialogOpen] = useState(false) + const [deleteOrgConfirm, setDeleteOrgConfirm] = useState("") + const deleteOrganization = useDeleteOrganization() + + // Only owners can delete the organization. + const activeMemberRoleQuery = useQuery({ + queryKey: ["organization", org?.id, "active-member-role"], + queryFn: async () => { + const result = await authClient.organization.getActiveMember() + if (result.error) { + throw new Error(result.error.message ?? "Failed to load team role") + } + return result.data?.role ?? null + }, + enabled: !!org?.id, + staleTime: 60_000, + }) + const isOwner = (activeMemberRoleQuery.data ?? "").toLowerCase() === "owner" + const [dangerMenuOpen, setDangerMenuOpen] = useState(false) const displayName = @@ -174,6 +196,24 @@ export function SettingsContent({ ) } + const handleDeleteOrganization = async () => { + if (!org || deleteOrgConfirm !== org.name) return + deleteOrganization.mutate(org.id, { + onSuccess: async () => { + setIsDeleteOrgDialogOpen(false) + setDeleteOrgConfirm("") + const remaining = (organizations ?? []).filter((o) => o.id !== org.id) + if (remaining[0]) { + await setActiveOrg(remaining[0].slug) + window.location.href = "/" + } else { + await clearActiveOrg() + window.location.href = "/onboarding" + } + }, + }) + } + return ( <>
Reset data + {isOwner && ( + <> +
+ + + + )} +
+ + +
+
+ + ) } diff --git a/apps/web/hooks/use-delete-organization.ts b/apps/web/hooks/use-delete-organization.ts new file mode 100644 index 00000000..b829d20b --- /dev/null +++ b/apps/web/hooks/use-delete-organization.ts @@ -0,0 +1,26 @@ +"use client" + +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" +import { authClient } from "@lib/auth" + +export function useDeleteOrganization() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (organizationId: string) => { + const res = await authClient.organization.delete({ organizationId }) + if (res?.error) { + throw new Error(res.error.message || "Failed to delete organization") + } + return res?.data + }, + onSuccess: () => { + queryClient.invalidateQueries() + toast.success("Organization deleted.") + }, + onError: (error: Error) => { + toast.error(error.message || "Failed to delete organization.") + }, + }) +}