From 40519683c8ef350cc3f7681a12fa99951ba83fc2 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Mon, 24 Aug 2026 12:17:28 +0530 Subject: [PATCH] fix plugin organization authorization flow --- apps/web/app/auth/connect/page.tsx | 129 +++++++++++++++-------------- packages/lib/auth-context.tsx | 9 +- 2 files changed, 71 insertions(+), 67 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index f4e52289..7e9dd5cf 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -193,13 +193,10 @@ function AuthConnectContent() { const params = useSearchParams() const router = useRouter() const { data: session, isPending } = useSession() - const { organizations, isRestoring, setActiveOrg } = useAuth() + const { organizations, isRestoring } = useAuth() const [status, setStatus] = useState("loading") const [error, setError] = useState(null) const [selectedOrgId, setSelectedOrgId] = useState(null) - const [switchingOrgId, setSwitchingOrgId] = useState(null) - const switchingOrgIdRef = useRef(null) - const autoAttemptedOrgId = useRef(null) const listRef = useRef(null) const [canScrollUp, setCanScrollUp] = useState(false) const [canScrollDown, setCanScrollDown] = useState(false) @@ -267,26 +264,12 @@ function AuthConnectContent() { }, [isPending, isRestoring, session, organizations, router, requestError]) const selectOrganization = useCallback( - async (organization: NonNullable[number]) => { - if (switchingOrgIdRef.current) return - + (organization: NonNullable[number]) => { setError(null) - switchingOrgIdRef.current = organization.id - setSwitchingOrgId(organization.id) - try { - await setActiveOrg(organization.slug) - setSelectedOrgId(organization.id) - setStatus("approval") - } catch (err) { - console.error("Failed to switch organization:", err) - setError("Couldn't switch to that organization. Try again.") - setStatus("selection") - } finally { - switchingOrgIdRef.current = null - setSwitchingOrgId(null) - } + setSelectedOrgId(organization.id) + setStatus("approval") }, - [setActiveOrg], + [], ) useEffect(() => { @@ -298,15 +281,7 @@ function AuthConnectContent() { return } const onlyOrganization = organizations[0] - if ( - !onlyOrganization || - autoAttemptedOrgId.current === onlyOrganization.id - ) { - return - } - autoAttemptedOrgId.current = onlyOrganization.id - setStatus("selection") - void selectOrganization(onlyOrganization) + if (onlyOrganization) selectOrganization(onlyOrganization) }, [ requestError, isPending, @@ -358,39 +333,77 @@ function AuthConnectContent() { try { setError(null) setStatus("creating") - const fetchParams = new URLSearchParams({ callback }) - fetchParams.set("client", requestedClients[0] ?? "") - fetchParams.set("orgId", selectedOrg.id) + const keyResults = await Promise.allSettled( + requestedClients.map(async (requestedClient) => { + const fetchParams = new URLSearchParams({ + callback, + client: requestedClient, + orgId: selectedOrg.id, + }) + const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { + credentials: "include", + }) - const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { - credentials: "include", - }) + if (!res.ok) { + const errorData = (await res.json().catch(() => ({}))) as { + message?: string + } + throw new Error(errorData.message || "Failed to get API key") + } - if (!res.ok) { - const errorData = (await res.json().catch(() => ({}))) as { - message?: string + const data = (await res.json()) as { + key: string + organization?: { id: string } + } + const expectedKeyPrefix = `sm_${selectedOrg.id}_` + if ( + (data.organization && data.organization.id !== selectedOrg.id) || + !data.key.startsWith(expectedKeyPrefix) + ) { + throw new Error( + "The server did not create a key for the selected organization. Try again shortly.", + ) + } + + return [requestedClient, data.key] as const + }), + ) + const keys: Record = {} + const errors: Record = {} + for (const [index, result] of keyResults.entries()) { + const requestedClient = requestedClients[index] + if (!requestedClient) continue + if (result.status === "fulfilled") { + keys[result.value[0]] = result.value[1] + } else { + errors[requestedClient] = + result.reason instanceof Error + ? result.reason.message + : "Failed to get API key" } - throw new Error(errorData.message || "Failed to get API key") } - const data = (await res.json()) as { key: string } + if (!hasClientList && Object.keys(errors).length > 0) { + throw new Error(errors[requestedClients[0] ?? ""]) + } + if (Object.keys(keys).length === 0) { + throw new Error( + Object.values(errors)[0] ?? "Failed to get plugin API keys", + ) + } setStatus("success") const redirectUrl = new URL(callback) if (hasClientList) { - redirectUrl.searchParams.set( - "keys", - encodeBase64UrlJson( - Object.fromEntries( - requestedClients.map((requestedClient) => [ - requestedClient, - data.key, - ]), - ), - ), - ) + redirectUrl.searchParams.set("keys", encodeBase64UrlJson(keys)) + if (Object.keys(errors).length > 0) { + redirectUrl.searchParams.set("errors", encodeBase64UrlJson(errors)) + } } else { - redirectUrl.searchParams.set("apikey", data.key) + redirectUrl.searchParams.set( + "apikey", + keys[requestedClients[0] ?? ""] ?? "", + ) } redirectUrl.searchParams.set("api_url", API_URL) window.location.href = redirectUrl.toString() @@ -482,10 +495,9 @@ function AuthConnectContent() { {organizations?.map((organization) => ( ))} diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index 7234975e..acd15e88 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -75,13 +75,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { const res = await authClient.organization.setActive({ organizationSlug: slug, }) - if (res.error || !res.data) { - throw new Error(res.error?.message ?? "Failed to switch organization") - } - try { - localStorage.setItem(STORAGE_KEY, slug) - } catch {} - setOrg(res.data) + setOrg(res?.data ?? null) + localStorage.setItem(STORAGE_KEY, slug) }, []) const clearActiveOrg = useCallback(async () => {