mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
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:
1 parent
f393585ce0
commit
f2f03ce15e
2 files changed
+54
-3
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user