From fce2ccb9d13ad0e9907b89bcbd3ea8dc284d9dc9 Mon Sep 17 00:00:00 2001
From: Vorflux AI
Date: Sun, 23 Aug 2026 22:35:52 +0000
Subject: [PATCH 1/7] feat(web): select organization during plugin auth
---
apps/web/app/auth/connect/page.tsx | 475 ++++++++++++++++++++---------
packages/lib/auth-context.tsx | 9 +-
2 files changed, 334 insertions(+), 150 deletions(-)
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx
index febd2760..f4e52289 100644
--- a/apps/web/app/auth/connect/page.tsx
+++ b/apps/web/app/auth/connect/page.tsx
@@ -3,11 +3,19 @@
import { useAuth } from "@lib/auth-context"
import { useSession } from "@lib/auth"
import { cn } from "@lib/utils"
+import { Logo } from "@ui/assets/Logo"
import { dmSans125ClassName } from "@/lib/fonts"
-import { ArrowRight, XCircle } from "lucide-react"
+import { ArrowLeft, ArrowRight, LoaderIcon, XCircle } from "lucide-react"
import Image from "next/image"
import { useRouter, useSearchParams } from "next/navigation"
-import { Suspense, useEffect, useMemo, useState } from "react"
+import {
+ Suspense,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react"
import { PENDING_CONNECT_URL_KEY } from "@/lib/constants"
@@ -172,7 +180,7 @@ function PluginLogoStack({ clients }: { clients: string[] }) {
)
}
-type Status = "loading" | "creating" | "success" | "error"
+type Status = "loading" | "selection" | "approval" | "creating" | "success"
const pageWrapperClass =
"flex items-center justify-center min-h-screen bg-background p-4"
@@ -185,9 +193,16 @@ function AuthConnectContent() {
const params = useSearchParams()
const router = useRouter()
const { data: session, isPending } = useSession()
- const { org, organizations, isRestoring } = useAuth()
+ const { organizations, isRestoring, setActiveOrg } = 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)
const callback = params.get("callback")
const client = params.get("client")
@@ -214,10 +229,22 @@ function AuthConnectContent() {
requestedClients.length === 1 && validClient
? PLUGIN_INFO[validClient]
: null
+ const requestError = useMemo(() => {
+ if (!callback) return "Missing callback parameter."
+ if (!isValidLocalhostCallback(callback)) return "Invalid callback URL."
+ if (invalidClients.length > 0) {
+ return `Unsupported plugin requested: ${invalidClients.join(", ")}.`
+ }
+ if (requestedClients.length === 0) return "Invalid or missing client."
+ return null
+ }, [callback, invalidClients, requestedClients.length])
+ const selectedOrg =
+ organizations?.find((organization) => organization.id === selectedOrgId) ??
+ null
+ const multiOrg = (organizations?.length ?? 0) > 1
- // Redirect new users (logged in but no organization) to onboarding.
- // Store the current connect URL so onboarding can redirect back here.
const shouldRedirectToOnboarding =
+ !requestError &&
!isPending &&
!isRestoring &&
!!session &&
@@ -225,6 +252,7 @@ function AuthConnectContent() {
organizations.length === 0
useEffect(() => {
+ if (requestError) return
if (isPending || isRestoring) return
if (!session) return
if (organizations === null) return // orgs query still pending
@@ -236,41 +264,103 @@ function AuthConnectContent() {
console.warn("Failed to access sessionStorage for pending connect URL", e)
}
router.replace("/onboarding")
- }, [isPending, isRestoring, session, organizations, router])
+ }, [isPending, isRestoring, session, organizations, router, requestError])
+
+ const selectOrganization = useCallback(
+ async (organization: NonNullable[number]) => {
+ if (switchingOrgIdRef.current) return
+
+ 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)
+ }
+ },
+ [setActiveOrg],
+ )
+
+ useEffect(() => {
+ if (requestError || isPending || isRestoring || organizations === null)
+ return
+ if (!session || organizations.length === 0 || status !== "loading") return
+ if (organizations.length > 1) {
+ setStatus("selection")
+ return
+ }
+ const onlyOrganization = organizations[0]
+ if (
+ !onlyOrganization ||
+ autoAttemptedOrgId.current === onlyOrganization.id
+ ) {
+ return
+ }
+ autoAttemptedOrgId.current = onlyOrganization.id
+ setStatus("selection")
+ void selectOrganization(onlyOrganization)
+ }, [
+ requestError,
+ isPending,
+ isRestoring,
+ organizations,
+ session,
+ status,
+ selectOrganization,
+ ])
+
+ useEffect(() => {
+ if (status !== "approval" || !selectedOrgId || organizations === null)
+ return
+ if (
+ organizations.some((organization) => organization.id === selectedOrgId)
+ ) {
+ return
+ }
+ setSelectedOrgId(null)
+ setError("That organization is no longer available. Choose another one.")
+ setStatus("selection")
+ }, [organizations, selectedOrgId, status])
+
+ const measureFades = useCallback((element: HTMLDivElement | null) => {
+ if (!element) return
+ setCanScrollUp(element.scrollTop > 8)
+ setCanScrollDown(
+ element.scrollTop + element.clientHeight < element.scrollHeight - 8,
+ )
+ }, [])
+
+ useEffect(() => {
+ if (status !== "selection") return
+ measureFades(listRef.current)
+ }, [measureFades, status])
async function handleConnect() {
- if (!callback) {
- setStatus("error")
- setError("Missing callback parameter.")
- return
- }
- if (!isValidLocalhostCallback(callback)) {
- setStatus("error")
- setError("Invalid callback URL.")
- return
- }
- if (invalidClients.length > 0) {
- setStatus("error")
- setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`)
- return
- }
- if (requestedClients.length === 0) {
- setStatus("error")
- setError("Invalid or missing client.")
- return
- }
- if (!session || !org) {
- setStatus("error")
+ if (requestError || !callback) return
+ if (!session || !selectedOrg) {
setError(
- "Your account is not fully set up yet. Please complete onboarding first.",
+ selectedOrgId
+ ? "That organization is no longer available. Choose another one."
+ : "Select an organization before approving the connection.",
)
+ setStatus(multiOrg ? "selection" : "approval")
return
}
try {
+ setError(null)
setStatus("creating")
const fetchParams = new URLSearchParams({ callback })
fetchParams.set("client", requestedClients[0] ?? "")
+ fetchParams.set("orgId", selectedOrg.id)
const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, {
credentials: "include",
@@ -306,105 +396,14 @@ function AuthConnectContent() {
window.location.href = redirectUrl.toString()
} catch (err) {
console.error("Failed to get API key:", err)
- setStatus("error")
setError(err instanceof Error ? err.message : "Failed to get API key")
+ setStatus("approval")
}
}
- // Show a spinner while session/org data is loading or while we're about
- // to redirect to onboarding (prevents a brief flash of the connect card).
const isAuthLoading = isPending || isRestoring || organizations === null
- useEffect(() => {
- if (status !== "loading") return
- if (rawRequestedClients.length === 0) {
- setStatus("error")
- setError("Invalid or missing client.")
- return
- }
- if (invalidClients.length > 0) {
- setStatus("error")
- setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`)
- }
- }, [invalidClients, rawRequestedClients.length, status])
-
- if (isAuthLoading || shouldRedirectToOnboarding) {
- return (
-
- )
- }
-
- if (status === "loading") {
- return (
-
-
-
-
-
-
- Connect {displayName}
-
-
- {pluginInfo?.description ??
- (requestedClients.length > 1
- ? "Use one Supermemory account across these plugins."
- : `Use your Supermemory account with ${displayName}.`)}
-
-
-
-
- {(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map(
- (feature) => (
-
-
-
- {feature}
-
-
- ),
- )}
-
-
-
- Approve Connection
-
-
-
-
-
- )
- }
- if (status === "error") {
+ if (requestError) {
return (
@@ -413,42 +412,223 @@ function AuthConnectContent() {
Connection failed
- {error}
+ {requestError}
+
+ Go to app
+
+
+
+
+ )
+ }
-
-
void handleConnect()}
- className={cn(
- "w-full flex items-center justify-center gap-2 rounded-full h-10 px-4",
- "bg-[#0D121A] border border-[#1E293B] text-[#FAFAFA]",
- "text-[13px] font-medium cursor-pointer transition-colors hover:bg-[#1E293B]",
- dmSans125ClassName(),
- )}
- >
- Try again
-
-
- Go to app
-
+ if (isAuthLoading || shouldRedirectToOnboarding || status === "loading") {
+ return (
+
+ )
+ }
+
+ if (status === "selection") {
+ return (
+
+
+
+
+
+
+ Select an organization
+
+
+ Choose which organization to connect {displayName} to.
+
+
+
+
measureFades(event.currentTarget)}
+ ref={listRef}
+ >
+ {organizations?.map((organization) => (
+
void selectOrganization(organization)}
+ type="button"
+ >
+
+ {organization.name.charAt(0).toUpperCase() || "?"}
+
+
+ {organization.name}
+
+ {switchingOrgId === organization.id && (
+
+ )}
+
+ ))}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ )
+ }
+
+ if (status === "approval" || status === "creating") {
+ const creating = status === "creating"
+ return (
+
+
+
+
+
+
+ Connect {displayName}
+
+
+ {pluginInfo?.description ??
+ (requestedClients.length > 1
+ ? "Use one Supermemory account across these plugins."
+ : `Use your Supermemory account with ${displayName}.`)}
+
+
+
+ {(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map((feature) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+
+
+ Connecting to
+
+
+ {selectedOrg?.name ?? "Organization unavailable"}
+
+
+ {multiOrg && (
+
{
+ setError(null)
+ setStatus("selection")
+ }}
+ type="button"
+ >
+
+ Change
+
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
void handleConnect()}
+ style={{
+ background:
+ "linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)",
+ boxShadow:
+ "1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)",
+ }}
+ type="button"
+ >
+ {creating ? (
+ <>
+
+ Creating connection
+ >
+ ) : (
+ "Approve Connection"
+ )}
+
+
@@ -460,7 +640,6 @@ function AuthConnectContent() {
- {status === "creating" && `Connecting ${displayName}…`}
{status === "success" &&
`Success! Redirecting back to ${displayName}…`}
diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx
index acd15e88..7234975e 100644
--- a/packages/lib/auth-context.tsx
+++ b/packages/lib/auth-context.tsx
@@ -75,8 +75,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const res = await authClient.organization.setActive({
organizationSlug: slug,
})
- setOrg(res?.data ?? null)
- localStorage.setItem(STORAGE_KEY, 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)
}, [])
const clearActiveOrg = useCallback(async () => {
From 40519683c8ef350cc3f7681a12fa99951ba83fc2 Mon Sep 17 00:00:00 2001
From: Ishaan Gupta
Date: Mon, 24 Aug 2026 12:17:28 +0530
Subject: [PATCH 2/7] 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) => (
void selectOrganization(organization)}
+ onClick={() => selectOrganization(organization)}
type="button"
>
@@ -494,9 +506,6 @@ function AuthConnectContent() {
{organization.name}
- {switchingOrgId === organization.id && (
-
- )}
))}
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 () => {
From 22cf75912db6060e7ed4ec0a36c783899d1e085f Mon Sep 17 00:00:00 2001
From: Ishaan Gupta
Date: Mon, 24 Aug 2026 12:33:33 +0530
Subject: [PATCH 3/7] handle expired plugin connect sessions
---
apps/web/app/auth/connect/page.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx
index 7e9dd5cf..8ca89f37 100644
--- a/apps/web/app/auth/connect/page.tsx
+++ b/apps/web/app/auth/connect/page.tsx
@@ -248,6 +248,13 @@ function AuthConnectContent() {
Array.isArray(organizations) &&
organizations.length === 0
+ useEffect(() => {
+ if (requestError || isPending || isRestoring || session) return
+ router.replace(
+ `/login?redirect=${encodeURIComponent(window.location.href)}`,
+ )
+ }, [isPending, isRestoring, requestError, router, session])
+
useEffect(() => {
if (requestError) return
if (isPending || isRestoring) return
From 86fbb823a2037d12e4a26c6a74713124d09b0cd5 Mon Sep 17 00:00:00 2001
From: Ishaan Gupta
Date: Mon, 24 Aug 2026 19:05:34 +0530
Subject: [PATCH 4/7] refine plugin organization authorization UX
---
apps/web/app/auth/connect/page.tsx | 95 +++++++++++++++++++-----------
1 file changed, 59 insertions(+), 36 deletions(-)
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx
index 8ca89f37..bbd6bb95 100644
--- a/apps/web/app/auth/connect/page.tsx
+++ b/apps/web/app/auth/connect/page.tsx
@@ -5,7 +5,7 @@ import { useSession } from "@lib/auth"
import { cn } from "@lib/utils"
import { Logo } from "@ui/assets/Logo"
import { dmSans125ClassName } from "@/lib/fonts"
-import { ArrowLeft, ArrowRight, LoaderIcon, XCircle } from "lucide-react"
+import { ArrowRight, LoaderIcon, XCircle } from "lucide-react"
import Image from "next/image"
import { useRouter, useSearchParams } from "next/navigation"
import {
@@ -193,7 +193,7 @@ function AuthConnectContent() {
const params = useSearchParams()
const router = useRouter()
const { data: session, isPending } = useSession()
- const { organizations, isRestoring } = useAuth()
+ const { org, organizations, isRestoring } = useAuth()
const [status, setStatus] = useState("loading")
const [error, setError] = useState(null)
const [selectedOrgId, setSelectedOrgId] = useState(null)
@@ -205,6 +205,7 @@ function AuthConnectContent() {
const client = params.get("client")
const clientsParam = params.get("clients")
const hasClientList = params.has("clients")
+ const isSwitchMode = params.get("mode") === "switch_organization"
const rawRequestedClients = useMemo(
() =>
(clientsParam !== null ? clientsParam.split(",") : client ? [client] : [])
@@ -270,33 +271,31 @@ function AuthConnectContent() {
router.replace("/onboarding")
}, [isPending, isRestoring, session, organizations, router, requestError])
- const selectOrganization = useCallback(
- (organization: NonNullable[number]) => {
- setError(null)
- setSelectedOrgId(organization.id)
- setStatus("approval")
- },
- [],
- )
-
useEffect(() => {
if (requestError || isPending || isRestoring || organizations === null)
return
if (!session || organizations.length === 0 || status !== "loading") return
- if (organizations.length > 1) {
+ if (isSwitchMode) {
setStatus("selection")
return
}
- const onlyOrganization = organizations[0]
- if (onlyOrganization) selectOrganization(onlyOrganization)
+
+ const defaultOrganization =
+ organizations.find((organization) => organization.id === org?.id) ??
+ organizations[0]
+ if (defaultOrganization) {
+ setSelectedOrgId(defaultOrganization.id)
+ setStatus("approval")
+ }
}, [
+ isSwitchMode,
requestError,
isPending,
isRestoring,
+ org?.id,
organizations,
session,
status,
- selectOrganization,
])
useEffect(() => {
@@ -325,9 +324,9 @@ function AuthConnectContent() {
measureFades(listRef.current)
}, [measureFades, status])
- async function handleConnect() {
+ async function handleConnect(organization = selectedOrg): Promise {
if (requestError || !callback) return
- if (!session || !selectedOrg) {
+ if (!session || !organization) {
setError(
selectedOrgId
? "That organization is no longer available. Choose another one."
@@ -345,7 +344,7 @@ function AuthConnectContent() {
const fetchParams = new URLSearchParams({
callback,
client: requestedClient,
- orgId: selectedOrg.id,
+ orgId: organization.id,
})
const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, {
credentials: "include",
@@ -362,9 +361,9 @@ function AuthConnectContent() {
key: string
organization?: { id: string }
}
- const expectedKeyPrefix = `sm_${selectedOrg.id}_`
+ const expectedKeyPrefix = `sm_${organization.id}_`
if (
- (data.organization && data.organization.id !== selectedOrg.id) ||
+ (data.organization && data.organization.id !== organization.id) ||
!data.key.startsWith(expectedKeyPrefix)
) {
throw new Error(
@@ -417,10 +416,22 @@ function AuthConnectContent() {
} catch (err) {
console.error("Failed to get API key:", err)
setError(err instanceof Error ? err.message : "Failed to get API key")
- setStatus("approval")
+ setStatus(isSwitchMode ? "selection" : "approval")
}
}
+ function selectOrganization(
+ organization: NonNullable[number],
+ ): void {
+ setError(null)
+ setSelectedOrgId(organization.id)
+ if (isSwitchMode) {
+ void handleConnect(organization)
+ return
+ }
+ setStatus("approval")
+ }
+
const isAuthLoading = isPending || isRestoring || organizations === null
if (requestError) {
@@ -545,6 +556,19 @@ function AuthConnectContent() {
)
}
+ if (isSwitchMode && status === "creating") {
+ return (
+
+
+
+
+ Switching organization…
+
+
+
+ )
+ }
+
if (status === "approval" || status === "creating") {
const creating = status === "creating"
return (
@@ -584,7 +608,7 @@ function AuthConnectContent() {
))}
-
+
Connecting to
@@ -593,20 +617,6 @@ function AuthConnectContent() {
{selectedOrg?.name ?? "Organization unavailable"}
- {multiOrg && (
-
{
- setError(null)
- setStatus("selection")
- }}
- type="button"
- >
-
- Change
-
- )}
{error && (
@@ -645,6 +655,19 @@ function AuthConnectContent() {
)}
+ {multiOrg && (
+
{
+ setError(null)
+ setStatus("selection")
+ }}
+ type="button"
+ >
+ Switch organization
+
+ )}
From 176f7d61cd4295408ad6e74584a6f357104a694d Mon Sep 17 00:00:00 2001
From: Ishaan Gupta
Date: Mon, 24 Aug 2026 19:34:43 +0530
Subject: [PATCH 5/7] restore inline organization change control
---
apps/web/app/auth/connect/page.tsx | 31 +++++++++++++++---------------
1 file changed, 16 insertions(+), 15 deletions(-)
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx
index bbd6bb95..3d135863 100644
--- a/apps/web/app/auth/connect/page.tsx
+++ b/apps/web/app/auth/connect/page.tsx
@@ -5,7 +5,7 @@ import { useSession } from "@lib/auth"
import { cn } from "@lib/utils"
import { Logo } from "@ui/assets/Logo"
import { dmSans125ClassName } from "@/lib/fonts"
-import { ArrowRight, LoaderIcon, XCircle } from "lucide-react"
+import { ArrowLeft, ArrowRight, LoaderIcon, XCircle } from "lucide-react"
import Image from "next/image"
import { useRouter, useSearchParams } from "next/navigation"
import {
@@ -608,7 +608,7 @@ function AuthConnectContent() {
))}
-
+
Connecting to
@@ -617,6 +617,20 @@ function AuthConnectContent() {
{selectedOrg?.name ?? "Organization unavailable"}
+ {multiOrg && (
+
{
+ setError(null)
+ setStatus("selection")
+ }}
+ type="button"
+ >
+
+ Change
+
+ )}
{error && (
@@ -655,19 +669,6 @@ function AuthConnectContent() {
)}
- {multiOrg && (
-
{
- setError(null)
- setStatus("selection")
- }}
- type="button"
- >
- Switch organization
-
- )}
From 83e7d8c776c16cb89a4a7d0bb15d13c6a33fcf51 Mon Sep 17 00:00:00 2001
From: Ishaan Gupta
Date: Mon, 24 Aug 2026 21:01:46 +0530
Subject: [PATCH 6/7] Show signed-in account on organization selector
---
apps/web/app/auth/connect/page.tsx | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx
index 3d135863..f69ce0b3 100644
--- a/apps/web/app/auth/connect/page.tsx
+++ b/apps/web/app/auth/connect/page.tsx
@@ -1,7 +1,7 @@
"use client"
import { useAuth } from "@lib/auth-context"
-import { useSession } from "@lib/auth"
+import { authClient, useSession } from "@lib/auth"
import { cn } from "@lib/utils"
import { Logo } from "@ui/assets/Logo"
import { dmSans125ClassName } from "@/lib/fonts"
@@ -324,6 +324,13 @@ function AuthConnectContent() {
measureFades(listRef.current)
}, [measureFades, status])
+ const handleSignOut = useCallback(async () => {
+ await authClient.signOut().catch(() => undefined)
+ router.replace(
+ `/login?redirect=${encodeURIComponent(window.location.href)}`,
+ )
+ }, [router])
+
async function handleConnect(organization = selectedOrg): Promise {
if (requestError || !callback) return
if (!session || !organization) {
@@ -551,6 +558,20 @@ function AuthConnectContent() {
{error}
)}
+
+ {session?.user.email && (
+
+ Signed in as {session.user.email}
+
+ )}
+
void handleSignOut()}
+ type="button"
+ >
+ Sign out
+
+
)
From 8f063bd2aeebd34fa58bab1a3fc35bd226485bb4 Mon Sep 17 00:00:00 2001
From: Ishaan Gupta
Date: Mon, 24 Aug 2026 21:43:14 +0530
Subject: [PATCH 7/7] Select organization before plugin approval
---
apps/web/app/auth/connect/page.tsx | 26 +++-----------------------
1 file changed, 3 insertions(+), 23 deletions(-)
diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx
index f69ce0b3..c3fd18ee 100644
--- a/apps/web/app/auth/connect/page.tsx
+++ b/apps/web/app/auth/connect/page.tsx
@@ -193,7 +193,7 @@ function AuthConnectContent() {
const params = useSearchParams()
const router = useRouter()
const { data: session, isPending } = useSession()
- const { org, organizations, isRestoring } = useAuth()
+ const { organizations, isRestoring } = useAuth()
const [status, setStatus] = useState("loading")
const [error, setError] = useState(null)
const [selectedOrgId, setSelectedOrgId] = useState(null)
@@ -275,28 +275,8 @@ function AuthConnectContent() {
if (requestError || isPending || isRestoring || organizations === null)
return
if (!session || organizations.length === 0 || status !== "loading") return
- if (isSwitchMode) {
- setStatus("selection")
- return
- }
-
- const defaultOrganization =
- organizations.find((organization) => organization.id === org?.id) ??
- organizations[0]
- if (defaultOrganization) {
- setSelectedOrgId(defaultOrganization.id)
- setStatus("approval")
- }
- }, [
- isSwitchMode,
- requestError,
- isPending,
- isRestoring,
- org?.id,
- organizations,
- session,
- status,
- ])
+ setStatus("selection")
+ }, [requestError, isPending, isRestoring, organizations, session, status])
useEffect(() => {
if (status !== "approval" || !selectedOrgId || organizations === null)