From 718c2bb033c5bc6e68b1ff5f47dd20d896dee817 Mon Sep 17 00:00:00 2001 From: midego1 Date: Thu, 6 Aug 2026 06:25:17 +0000 Subject: [PATCH 1/2] Confirm before revoking a personal API key Personal-key revoke deleted the key immediately with no confirmation, while org-key revoke (and every other destructive action) asks first. A personal key breaks any script or tool using it just the same, so add the same confirmation dialog, mirroring the org-key pattern. --- packages/react/src/pages/api-keys.tsx | 46 +++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/react/src/pages/api-keys.tsx b/packages/react/src/pages/api-keys.tsx index eb15084cb..b48e04856 100644 --- a/packages/react/src/pages/api-keys.tsx +++ b/packages/react/src/pages/api-keys.tsx @@ -233,6 +233,7 @@ export function ApiKeysPage(props: { readonly orgKeysSection?: ReactNode }) { // stays mounted for Radix's exit animation (see CreateKeyDialogBody). const [openCount, setOpenCount] = useState(0); const [revokingId, setRevokingId] = useState(null); + const [confirmRevoke, setConfirmRevoke] = useState(null); const handleCreate = async (name: string): Promise => { const exit = await doCreate({ payload: { name }, reactivityKeys: apiKeyWriteKeys }); @@ -312,7 +313,11 @@ export function ApiKeysPage(props: { readonly orgKeysSection?: ReactNode }) {

) : ( - + setConfirmRevoke(key)} + /> ), }) )} @@ -333,6 +338,42 @@ export function ApiKeysPage(props: { readonly orgKeysSection?: ReactNode }) { /> + + {/* A personal key revoke breaks any script or tool using it, so it asks + first — same as the org-key revoke. */} + { + if (!open) setConfirmRevoke(null); + }} + > + + + Revoke API key + + {confirmRevoke + ? `Revoke ${confirmRevoke.name}? Any script or tool authenticating with it loses access immediately. This cannot be undone.` + : ""} + + + + + + + + + + ); } @@ -492,8 +533,7 @@ function OrgApiKeysSectionBody() { - {/* Revoking an org key breaks every backend using it, so it is the one - revoke on this page that asks first. */} + {/* Revoking an org key breaks every backend using it, so it asks first. */} { From 929b5121246068d721aacfc7ac9b7d4fcf143c9b Mon Sep 17 00:00:00 2001 From: midego1 Date: Thu, 6 Aug 2026 07:49:50 +0000 Subject: [PATCH 2/2] Confirm before removing a policy or an org member MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing a tool-access policy and removing an org member each fired instantly from a dropdown click, with no confirmation — unlike every other destructive action in the UI. Add the same AlertDialog confirm the connection and artifact removals already use (state held at the page level, since the dropdown menu unmounts a nested dialog on click). --- packages/react/src/pages/org.tsx | 51 ++++++++++++++++++++++++++- packages/react/src/pages/policies.tsx | 51 ++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/packages/react/src/pages/org.tsx b/packages/react/src/pages/org.tsx index 8797533af..670c0845f 100644 --- a/packages/react/src/pages/org.tsx +++ b/packages/react/src/pages/org.tsx @@ -14,6 +14,16 @@ import { DialogFooter, DialogClose, } from "../components/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../components/alert-dialog"; import { Button } from "../components/button"; import { PageContainer, PageHeader } from "../components/page"; import { Badge } from "../components/badge"; @@ -153,6 +163,7 @@ export function OrgPage(props: { const refreshMembers = useAtomRefresh(orgMembersAtom); const rolesResult = useAtomValue(orgRolesAtom); const doRemove = useAtomSet(removeMember, { mode: "promiseExit" }); + const [removingMember, setRemovingMember] = useState<{ id: string; name: string } | null>(null); const doUpdateRole = useAtomSet(updateMemberRole, { mode: "promiseExit" }); const doUpdateOrgName = useAtomSet(updateOrgName, { mode: "promiseExit" }); const [inviteOpen, setInviteOpen] = useState(false); @@ -179,6 +190,7 @@ export function OrgPage(props: { const showUpgradeOnInvite = atSeatLimit && !!props.upgradeAction; const handleRemove = async (membershipId: string, name: string) => { + setRemovingMember(null); const exit = await doRemove({ params: { membershipId }, reactivityKeys: orgMemberWriteKeys, @@ -406,7 +418,12 @@ export function OrgPage(props: { )} handleRemove(member.id, member.name ?? member.email)} + onClick={() => + setRemovingMember({ + id: member.id, + name: member.name ?? member.email, + }) + } > Remove member @@ -426,6 +443,38 @@ export function OrgPage(props: { {props.dangerZoneSection} + { + if (!open) setRemovingMember(null); + }} + > + + + + {removingMember ? `Remove ${removingMember.name}?` : "Remove member?"} + + + They lose access to this organization immediately. This cannot be undone; you would + need to invite them again. + + + + Cancel + { + if (removingMember !== null) { + void handleRemove(removingMember.id, removingMember.name); + } + }} + > + Remove member + + + + + (null); const ownerDisplay = useOwnerDisplay(); // Policies default to org/workspace. On local this is the hidden Local owner // that v1 local data migrates into. @@ -336,6 +351,7 @@ export function PoliciesPage() { }; const handleRemove = async (policy: { id: string; owner: Owner }) => { + setRemovingPolicy(null); const exit = await doRemove({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner }, @@ -462,7 +478,9 @@ export function PoliciesPage() { isFirst={!reorderable || j === 0} isLast={!reorderable || j === committed.length - 1} showOwnerLabel={ownerDisplay.showOwnerLabels} - onRemove={() => handleRemove({ id: p.id, owner: p.owner })} + onRemove={() => + setRemovingPolicy({ id: p.id, owner: p.owner, pattern: p.pattern }) + } onChangeAction={(action) => handleUpdate({ id: p.id, owner: p.owner }, action) } @@ -490,6 +508,37 @@ export function PoliciesPage() { }, }) )} + + { + if (!open) setRemovingPolicy(null); + }} + > + + + Remove policy? + + {removingPolicy + ? `The rule for "${removingPolicy.pattern}" will be deleted. Tools it governs fall back to the default policy. This cannot be undone.` + : ""} + + + + Cancel + { + if (removingPolicy !== null) { + void handleRemove({ id: removingPolicy.id, owner: removingPolicy.owner }); + } + }} + > + Remove policy + + + + ); }