Spaces:
Running
Running
import { useEffect, useState } from "react"; | |
import { useNavigate } from "react-router-dom"; | |
import { exchangeCodeForToken } from "../services/oauth"; | |
import { secureStorage } from "../utils/storage"; | |
import type { MCPServerConfig } from "../types/mcp"; | |
import { STORAGE_KEYS, DEFAULTS } from "../config/constants"; | |
interface OAuthTokens { | |
access_token: string; | |
refresh_token?: string; | |
expires_in?: number; | |
token_type?: string; | |
[key: string]: string | number | undefined; | |
} | |
interface OAuthCallbackProps { | |
serverUrl: string; | |
onSuccess?: (tokens: OAuthTokens) => void; | |
onError?: (error: Error) => void; | |
} | |
const OAuthCallback: React.FC<OAuthCallbackProps> = ({ | |
serverUrl, | |
onSuccess, | |
onError, | |
}) => { | |
const [status, setStatus] = useState<string>("Authorizing..."); | |
const navigate = useNavigate(); // Add this hook | |
useEffect(() => { | |
// Parse parameters from URL search params (OAuth providers send code in query string) | |
const parseHashParams = () => { | |
return new URLSearchParams(window.location.search); | |
}; | |
const params = parseHashParams(); | |
const code = params.get("code"); | |
const state = params.get("state"); | |
const error = params.get("error"); | |
// Verify state parameter for CSRF protection | |
const savedState = localStorage.getItem('oauth_state'); | |
if (state !== savedState) { | |
setStatus("Invalid state parameter. Possible CSRF attack."); | |
if (onError) onError(new Error("Invalid state parameter")); | |
return; | |
} | |
// Check for OAuth errors | |
if (error) { | |
const errorDescription = params.get("error_description") || error; | |
setStatus(`OAuth error: ${errorDescription}`); | |
if (onError) onError(new Error(errorDescription)); | |
return; | |
} | |
// Always persist MCP server URL for robustness | |
localStorage.setItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL, serverUrl); | |
if (code) { | |
exchangeCodeForToken({ | |
serverUrl, | |
code, | |
redirectUri: window.location.origin + "/#" + DEFAULTS.OAUTH_REDIRECT_PATH, // Add hash | |
}) | |
.then(async (tokens) => { | |
await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token); | |
// Add MCP server to MCPClientService for UI | |
const mcpServerUrl = localStorage.getItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL); | |
if (mcpServerUrl) { | |
const serverName = | |
localStorage.getItem(STORAGE_KEYS.MCP_SERVER_NAME) || mcpServerUrl; | |
const serverTransport = | |
(localStorage.getItem(STORAGE_KEYS.MCP_SERVER_TRANSPORT) as MCPServerConfig['transport']) || DEFAULTS.MCP_TRANSPORT; | |
const serverConfig = { | |
id: `server_${Date.now()}`, | |
name: serverName, | |
url: mcpServerUrl, | |
enabled: true, | |
transport: serverTransport, | |
auth: { | |
type: "bearer" as const, | |
token: tokens.access_token, | |
}, | |
}; | |
let servers: MCPServerConfig[] = []; | |
try { | |
const stored = localStorage.getItem(STORAGE_KEYS.MCP_SERVERS); | |
if (stored) servers = JSON.parse(stored); | |
} catch {} | |
const exists = servers.some((s: MCPServerConfig) => s.url === mcpServerUrl); | |
if (!exists) { | |
servers.push(serverConfig); | |
localStorage.setItem(STORAGE_KEYS.MCP_SERVERS, JSON.stringify(servers)); | |
} | |
// Clear temp values | |
localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_NAME); | |
localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_TRANSPORT); | |
localStorage.removeItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL); | |
} | |
// Clear OAuth state | |
localStorage.removeItem('oauth_state'); | |
setStatus("Authorization successful! Redirecting..."); | |
if (onSuccess) onSuccess(tokens); | |
// Use React Router navigation instead of window.location.replace | |
setTimeout(() => { | |
navigate("/", { replace: true }); | |
}, 1000); | |
}) | |
.catch((err) => { | |
setStatus("OAuth token exchange failed: " + err.message); | |
if (onError) onError(err); | |
// Clear OAuth state on error | |
localStorage.removeItem('oauth_state'); | |
}); | |
} else { | |
setStatus("Missing authorization code in callback URL."); | |
if (onError) onError(new Error("Missing authorization code")); | |
} | |
}, [serverUrl, onSuccess, onError, navigate]); | |
return ( | |
<div className="flex items-center justify-center min-h-screen"> | |
<div className="text-center"> | |
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div> | |
<p>{status}</p> | |
</div> | |
</div> | |
); | |
}; | |
export default OAuthCallback; |