fix(ui): use cloud logout for managed sign-out (#10937)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Paperclip supports self-hosted and Cloud-managed authenticated deployments > - A Cloud-managed tenant uses the Cloud harness to own the full browser session > - The account menu treated every authenticated deployment as Cloud and called the local sign-out API first > - This pull request uses the existing Cloud health metadata as the mode gate > - Cloud-managed sign-out now starts the harness logout round trip with a top-level navigation > - Self-hosted authenticated sign-out keeps the existing local API flow > - The benefit is a complete Cloud logout without changing self-hosted behavior ## Linked Issues or Issue Description Related prior work: Refs #10802. **What happened?** The account menu called the app-local sign-out endpoint before it moved an authenticated browser to the Cloud logout route. It also used authenticated deployment mode as the Cloud test. This test included self-hosted authenticated instances. **Expected behavior** A Cloud-managed tenant must navigate the top-level browser directly to `/cloud/logout`. A self-hosted authenticated instance must keep the app-local sign-out flow. **Steps to reproduce** 1. Open a Cloud-managed tenant. 2. Open the account menu. 3. Select **Sign out**. 4. Observe that the browser returns through the tenant auth route instead of completing the Cloud logout round trip. **Paperclip version or commit** Reproduced on `master` after `76f442040c`. **Deployment mode** Paperclip Cloud-managed authenticated deployment. ## What Changed - Read the existing Cloud instance metadata in the account menu. - Navigate directly to `/cloud/logout` for Cloud-managed instances without calling the local sign-out API. - Keep the local sign-out API and cache refresh for self-hosted authenticated instances. - Add regression coverage for both sides of the mode gate. ## Verification - `pnpm exec vitest run ui/src/components/SidebarAccountMenu.test.tsx` - `pnpm --filter @paperclipai/ui typecheck` - `pnpm check:token-gates` ## Risks - Low risk. The change is limited to the account-menu action. - The Cloud branch depends on the existing `health.cloud` metadata that already gates other Cloud UI behavior. - The self-hosted regression test verifies that authenticated mode alone does not select the Cloud route. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex based on GPT-5. The runtime provided agentic reasoning, repository tools, shell execution, and test execution. The exact internal model ID and context window are not exposed to the agent. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
f5e9ca3e89
commit
ac3b2e1d7a
|
|
@ -100,7 +100,7 @@ describe("SidebarAccountMenu", () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the signed-in user and opens the account card menu", async () => {
|
||||
it("keeps authenticated self-hosted sign-out on the local auth flow", async () => {
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
|
|
@ -167,19 +167,29 @@ describe("SidebarAccountMenu", () => {
|
|||
await flushReact();
|
||||
|
||||
expect(mockAuthApi.signOut).toHaveBeenCalledOnce();
|
||||
expect(mockNavigateTopLevel).toHaveBeenCalledWith("/cloud/logout");
|
||||
expect(mockNavigateTopLevel).not.toHaveBeenCalled();
|
||||
expect(queryClient.getQueryState(queryKeys.health)?.isInvalidated).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the managed logout route when sign-out omits a redirect", async () => {
|
||||
mockAuthApi.signOut.mockResolvedValue({ success: true });
|
||||
it("navigates cloud-managed sign-out through the harness without calling local auth", async () => {
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
queryClient.setQueryData(queryKeys.health, {
|
||||
status: "ok",
|
||||
deploymentMode: "authenticated",
|
||||
cloud: {
|
||||
managed: true,
|
||||
managedBy: "paperclip-cloud",
|
||||
stackSlug: "acme-labs",
|
||||
cloudBaseUrl: "https://cloud.example.test",
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
|
|
@ -198,6 +208,7 @@ describe("SidebarAccountMenu", () => {
|
|||
});
|
||||
await flushReact();
|
||||
|
||||
expect(mockAuthApi.signOut).not.toHaveBeenCalled();
|
||||
expect(mockNavigateTopLevel).toHaveBeenCalledWith("/cloud/logout");
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Link } from "@/lib/router";
|
|||
import { authApi } from "@/api/auth";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { navigateTopLevel } from "@/lib/browserNavigation";
|
||||
import { useCloudInstance } from "@/hooks/useCloudInstance";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
|
|
@ -120,6 +121,7 @@ export function SidebarAccountMenu({
|
|||
}: SidebarAccountMenuProps) {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const isCloud = Boolean(useCloudInstance());
|
||||
const { isMobile, setSidebarOpen, collapsed, peeking } = useSidebar();
|
||||
const rail = collapsed && !peeking;
|
||||
const open = controlledOpen ?? internalOpen;
|
||||
|
|
@ -132,12 +134,8 @@ export function SidebarAccountMenu({
|
|||
|
||||
const signOutMutation = useMutation({
|
||||
mutationFn: () => authApi.signOut(),
|
||||
onSuccess: async (result) => {
|
||||
onSuccess: async () => {
|
||||
setOpen(false);
|
||||
if (deploymentMode === "authenticated") {
|
||||
navigateTopLevel(result?.redirectTo?.trim() || MANAGED_SIGN_OUT_PATH);
|
||||
return;
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.health });
|
||||
},
|
||||
|
|
@ -161,6 +159,15 @@ export function SidebarAccountMenu({
|
|||
if (isMobile) setSidebarOpen(false);
|
||||
}
|
||||
|
||||
function handleSignOut() {
|
||||
if (isCloud) {
|
||||
closeNavigationChrome();
|
||||
navigateTopLevel(MANAGED_SIGN_OUT_PATH);
|
||||
return;
|
||||
}
|
||||
signOutMutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-t border-r border-border bg-background px-3 py-2">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
|
|
@ -269,7 +276,7 @@ export function SidebarAccountMenu({
|
|||
"flex w-full items-start gap-3 rounded-xl px-3 py-3 text-left transition-colors hover:bg-destructive/10",
|
||||
signOutMutation.isPending && "cursor-not-allowed opacity-60",
|
||||
)}
|
||||
onClick={() => signOutMutation.mutate()}
|
||||
onClick={handleSignOut}
|
||||
disabled={signOutMutation.isPending}
|
||||
>
|
||||
<span className="mt-0.5 rounded-lg border border-border bg-background/70 p-2 text-muted-foreground">
|
||||
|
|
|
|||
Loading…
Reference in New Issue