Unified · Split
| @@ -1,6 +1,7 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useEffect } from "react"; | 3 | import { useEffect, useRef } from "react"; |
| 4 | import { useQuery as useTanstackQuery } from "@tanstack/react-query"; | ||
| 4 | 5 | ||
| 5 | type PopupMessage = | 6 | type PopupMessage = |
| 6 | | { | 7 | | { |
| @@ -28,45 +29,47 @@ function fallbackErrorMessage(error: unknown): PopupMessage { · 21 unmodified lines | |||
| 28 | }; | 29 | }; |
| 29 | } | 30 | } |
| 30 | 31 | ||
| 32 | async function fetchPopupResult(): Promise<PopupMessage> { | ||
| 33 | const response = await fetch("/mcp/oauth/result", { | ||
| 34 | method: "GET", | ||
| 35 | cache: "no-store", | ||
| 36 | credentials: "same-origin", | ||
| 37 | }); | ||
| 38 | |||
| 39 | const data = await response.json() as PopupMessage; | ||
| 40 | if (!data || data.type !== "executor:mcp-oauth-result") { | ||
| 41 | throw new Error("Invalid OAuth completion payload"); | ||
| 42 | } | ||
| 43 | return data; | ||
| 44 | } | ||
| 45 | |||
| 31 | export default function McpOAuthCompletePage() { | 46 | export default function McpOAuthCompletePage() { |
| 32 | useEffect(() => { | 47 | const sentRef = useRef(false); |
| 33 | let cancelled = false; | 48 | const resultQuery = useTanstackQuery({ |
| 49 | queryKey: ["mcp-oauth-popup-result"], | ||
| 50 | queryFn: fetchPopupResult, | ||
| 51 | retry: false, | ||
| 52 | staleTime: 0, | ||
| 53 | }); | ||
| 34 | 54 | ||
| 35 | const finish = async () => { | 55 | useEffect(() => { |
| 36 | let message: PopupMessage; | 56 | if (sentRef.current || resultQuery.isPending) { |
| 37 | try { | 57 | return; |
| 38 | const response = await fetch("/mcp/oauth/result", { | 58 | } |
| 39 | method: "GET", | ||
| 40 | cache: "no-store", | ||
| 41 | credentials: "same-origin", | ||
| 42 | }); | ||
| 43 | const data = await response.json() as PopupMessage; | ||
| 44 | if (!data || data.type !== "executor:mcp-oauth-result") { | ||
| 45 | throw new Error("Invalid OAuth completion payload"); | ||
| 46 | } | ||
| 47 | message = data; | ||
| 48 | } catch (error) { | ||
| 49 | message = fallbackErrorMessage(error); | ||
| 50 | } | ||
| 51 | 59 | ||
| 52 | if (cancelled) { | 60 | sentRef.current = true; |
| 53 | return; | 61 | const message = resultQuery.isSuccess |
| 54 | } | 62 | ? resultQuery.data |
| 63 | : fallbackErrorMessage(resultQuery.error); | ||
| 55 | 64 | ||
| 56 | try { | 65 | try { |
| 57 | if (window.opener) { | 66 | if (window.opener) { |
| 58 | window.opener.postMessage(message, window.location.origin); | 67 | window.opener.postMessage(message, window.location.origin); |
| 59 | } | ||
| 60 | } finally { | ||
| 61 | window.close(); | ||
| 62 | } | 68 | } |
| 63 | }; | 69 | } finally { |
| 64 | 70 | window.close(); | |
| 65 | void finish(); | 71 | } |
| 66 | return () => { | 72 | }, [resultQuery.data, resultQuery.error, resultQuery.isPending, resultQuery.isSuccess]); |
| 67 | cancelled = true; | ||
| 68 | }; | ||
| 69 | }, []); | ||
| 70 | 73 | ||
| 71 | return ( | 74 | return ( |
| 72 | <main className="min-h-screen flex items-center justify-center bg-background p-6"> | 75 | <main className="min-h-screen flex items-center justify-center bg-background p-6"> |
| @@ -98,6 +98,7 @@ export function SourceAuthPanel({ · 97 unmodified lines | |||
| 98 | } | 98 | } |
| 99 | 99 | ||
| 100 | const badge = inferredAuthBadge(inferredSpecAuth); | 100 | const badge = inferredAuthBadge(inferredSpecAuth); |
| 101 | const mcpBearerConnected = sourceType === "mcp" && authType === "bearer" && mcpOAuthConnected; | ||
| 101 | return ( | 102 | return ( |
| 102 | <div className="space-y-3"> | 103 | <div className="space-y-3"> |
| 103 | 104 | ||
| @@ -179,7 +180,7 @@ export function SourceAuthPanel({ · 75 unmodified lines | |||
| 179 | <div className="space-y-1.5"> | 180 | <div className="space-y-1.5"> |
| 180 | <div className="flex items-center gap-2"> | 181 | <div className="flex items-center gap-2"> |
| 181 | <Label className="text-xs text-muted-foreground">OAuth</Label> | 182 | <Label className="text-xs text-muted-foreground">OAuth</Label> |
| 182 | {mcpOAuthConnected ? ( | 183 | {mcpBearerConnected ? ( |
| 183 | <Badge variant="outline" className="text-[10px] uppercase tracking-wider text-terminal-green"> | 184 | <Badge variant="outline" className="text-[10px] uppercase tracking-wider text-terminal-green"> |
| 184 | Connected | 185 | Connected |
| 185 | </Badge> | 186 | </Badge> |
| @@ -193,8 +194,11 @@ export function SourceAuthPanel({ · 7 unmodified lines | |||
| 193 | disabled={mcpOAuthBusy} | 194 | disabled={mcpOAuthBusy} |
| 194 | onClick={onMcpOAuthConnect} | 195 | onClick={onMcpOAuthConnect} |
| 195 | > | 196 | > |
| 196 | {mcpOAuthBusy ? "Connecting..." : mcpOAuthConnected ? "Reconnect OAuth" : "Connect OAuth in popup"} | 197 | {mcpOAuthBusy ? "Connecting..." : mcpBearerConnected ? "Reconnect OAuth" : "Connect OAuth in popup"} |
| 197 | </Button> | 198 | </Button> |
| 199 | {mcpBearerConnected ? ( | ||
| 200 | <p className="text-[11px] text-muted-foreground">OAuth linked successfully.</p> | ||
| 201 | ) : null} | ||
| 198 | </div> | 202 | </div> |
| 199 | ) : null} | 203 | ) : null} |
| 200 | 204 | ||
| @@ -210,7 +214,7 @@ export function SourceAuthPanel({ · 9 unmodified lines | |||
| 210 | </div> | 214 | </div> |
| 211 | ) : null} | 215 | ) : null} |
| 212 | 216 | ||
| 213 | {authType === "bearer" ? ( | 217 | {authType === "bearer" && !mcpBearerConnected ? ( |
| 214 | <div className="space-y-1.5"> | 218 | <div className="space-y-1.5"> |
| 215 | <Label className="text-xs text-muted-foreground flex items-center gap-1.5"> | 219 | <Label className="text-xs text-muted-foreground flex items-center gap-1.5"> |
| 216 | <LockKeyhole className="h-3 w-3" /> | 220 | <LockKeyhole className="h-3 w-3" /> |
| @@ -149,7 +149,7 @@ export function AddSourceDialog({ · 148 unmodified lines | |||
| 149 | form.handleAuthTypeChange("bearer"); | 149 | form.handleAuthTypeChange("bearer"); |
| 150 | } | 150 | } |
| 151 | form.handleAuthFieldChange("tokenValue", result.accessToken); | 151 | form.handleAuthFieldChange("tokenValue", result.accessToken); |
| 152 | toast.success("OAuth linked. Bearer token populated."); | 152 | toast.success("OAuth linked successfully."); |
| 153 | } catch (error) { | 153 | } catch (error) { |
| 154 | toast.error(error instanceof Error ? error.message : "Failed to connect OAuth"); | 154 | toast.error(error instanceof Error ? error.message : "Failed to connect OAuth"); |
| 155 | } finally { | 155 | } finally { |