|
|
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(); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
if (error) { |
|
|
const errorDescription = params.get("error_description") || error; |
|
|
setStatus(`OAuth error: ${errorDescription}`); |
|
|
if (onError) onError(new Error(errorDescription)); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
localStorage.setItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL, serverUrl); |
|
|
|
|
|
if (code) { |
|
|
exchangeCodeForToken({ |
|
|
serverUrl, |
|
|
code, |
|
|
redirectUri: window.location.origin + "/#" + DEFAULTS.OAUTH_REDIRECT_PATH, |
|
|
}) |
|
|
.then(async (tokens) => { |
|
|
await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token); |
|
|
|
|
|
|
|
|
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)); |
|
|
} |
|
|
|
|
|
|
|
|
localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_NAME); |
|
|
localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_TRANSPORT); |
|
|
localStorage.removeItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL); |
|
|
} |
|
|
|
|
|
|
|
|
localStorage.removeItem('oauth_state'); |
|
|
|
|
|
setStatus("Authorization successful! Redirecting..."); |
|
|
if (onSuccess) onSuccess(tokens); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
navigate("/", { replace: true }); |
|
|
}, 1000); |
|
|
}) |
|
|
.catch((err) => { |
|
|
setStatus("OAuth token exchange failed: " + err.message); |
|
|
if (onError) onError(err); |
|
|
|
|
|
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; |