Preserve company prefix in cloud upstream redirect URI

The cloud upstream connect flow built `redirectUri` and the post-finish
`replaceState` from the root path `/company/settings/cloud-upstream`,
dropping the company prefix (e.g. `/PAP/...`). On the 302 return from the
cloud, the React router had no company in context and bounced the user
to `/<defaultCompany>/dashboard`, dropping the `code`/`state` query
params and leaving the connection stuck at `token_status='pending'`.

Use `applyCompanyPrefix` against the selected company's issue prefix so
both the redirect URI and the URL cleanup land back on the same
company-scoped settings route.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-19 13:54:54 -05:00
1 parent f393585ce0
commit f2f03ce15e
2 files changed
+54 -3

No files matched your search

+49 -1
View File
@@ -38,7 +38,7 @@ vi.mock("@/context/BreadcrumbContext", () => ({
vi.mock("@/context/CompanyContext", () => ({
useCompany: () => ({
selectedCompany: { id: "company-1", name: "Paperclip" },
selectedCompany: { id: "company-1", name: "Paperclip", issuePrefix: "PAP" },
selectedCompanyId: "company-1",
}),
}));
@@ -136,6 +136,54 @@ describe("CloudUpstream", () => {
});
});
it("sends a company-prefixed redirectUri when starting Connect", async () => {
mockCloudUpstreamsApi.list.mockResolvedValue({ connections: [], runs: [] });
mockCloudUpstreamsApi.startConnect.mockResolvedValue({
pendingConnectionId: "pending-1",
authorizationUrl: "https://cloud.example/upstream-consent?state=abc",
});
const root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<CloudUpstream />
</QueryClientProvider>,
);
});
await flushReact();
await flushReact();
const input = container.querySelector<HTMLInputElement>("input[aria-label='Paperclip Cloud stack URL']");
expect(input).toBeTruthy();
await act(async () => {
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
setter.call(input!, "https://cloud.example/PAP/dashboard");
input!.dispatchEvent(new Event("input", { bubbles: true }));
});
await flushReact();
const connectButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.trim() === "Connect") as HTMLButtonElement | undefined;
expect(connectButton).toBeTruthy();
await act(async () => {
connectButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
expect(mockCloudUpstreamsApi.startConnect).toHaveBeenCalledWith({
companyId: "company-1",
remoteUrl: "https://cloud.example/PAP/dashboard",
redirectUri: `${window.location.origin}/PAP/company/settings/cloud-upstream`,
});
await act(async () => {
root.unmount();
});
});
it("keeps retry only for failed or cancelled runs", async () => {
mockCloudUpstreamsApi.list.mockResolvedValue(stateWithRun(buildRun({ status: "failed" })));
const root = createRoot(container);
+5 -2
View File
@@ -24,6 +24,7 @@ import { cloudUpstreamsApi } from "@/api/cloudUpstreams";
import { instanceSettingsApi } from "@/api/instanceSettings";
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
import { useCompany } from "@/context/CompanyContext";
import { applyCompanyPrefix } from "@/lib/company-routes";
import { Link, useLocation } from "@/lib/router";
import { queryKeys } from "@/lib/queryKeys";
@@ -101,6 +102,8 @@ export function CloudUpstream() {
const state = callbackParams.get("state");
const callbackError = callbackParams.get("error");
const settingsPath = applyCompanyPrefix("/company/settings/cloud-upstream", selectedCompany?.issuePrefix ?? null);
const finishMutation = useMutation({
mutationFn: (input: { pendingConnectionId: string; code: string; state: string }) =>
cloudUpstreamsApi.finishConnect(input),
@@ -109,7 +112,7 @@ export function CloudUpstream() {
setNotice("Cloud upstream connection approved.");
setActionError(null);
await invalidateUpstreams();
window.history.replaceState(null, "", "/company/settings/cloud-upstream");
window.history.replaceState(null, "", settingsPath);
},
onError: (error) => setActionError(error instanceof Error ? error.message : "Failed to finish connection."),
});
@@ -135,7 +138,7 @@ export function CloudUpstream() {
cloudUpstreamsApi.startConnect({
companyId: selectedCompanyId!,
remoteUrl,
redirectUri: `${window.location.origin}/company/settings/cloud-upstream`,
redirectUri: `${window.location.origin}${settingsPath}`,
}),
onSuccess: (result) => {
localStorage.setItem(PENDING_CONNECTION_KEY, result.pendingConnectionId);