LiquidAI/LFM2-MCP
39
1import { useEffect, useState } from "react";2import { useNavigate } from "react-router-dom";3import { exchangeCodeForToken } from "../services/oauth";4import { secureStorage } from "../utils/storage";5import type { MCPServerConfig } from "../types/mcp";6import { STORAGE_KEYS, DEFAULTS } from "../config/constants";7 8interface OAuthTokens {9 access_token: string;10 refresh_token?: string;11 expires_in?: number;12 token_type?: string;13 [key: string]: string | number | undefined;14}15 16interface OAuthCallbackProps {17 serverUrl: string;18 onSuccess?: (tokens: OAuthTokens) => void;19 onError?: (error: Error) => void;20}21 22const OAuthCallback: React.FC<OAuthCallbackProps> = ({23 serverUrl,24 onSuccess,25 onError,26}) => {27 const [status, setStatus] = useState<string>("Authorizing...");28 const navigate = useNavigate(); // Add this hook29 30 useEffect(() => {31 // Parse parameters from URL search params (OAuth providers send code in query string)32 const parseHashParams = () => {33 return new URLSearchParams(window.location.search);34 };35 36 const params = parseHashParams();37 const code = params.get("code");38 const state = params.get("state");39 const error = params.get("error");40 41 // Verify state parameter for CSRF protection42 const savedState = localStorage.getItem('oauth_state');43 if (state !== savedState) {44 setStatus("Invalid state parameter. Possible CSRF attack.");45 if (onError) onError(new Error("Invalid state parameter"));46 return;47 }48 49 // Check for OAuth errors50 if (error) {51 const errorDescription = params.get("error_description") || error;52 setStatus(`OAuth error: ${errorDescription}`);53 if (onError) onError(new Error(errorDescription));54 return;55 }56 57 // Always persist MCP server URL for robustness58 localStorage.setItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL, serverUrl);59 60 if (code) {61 exchangeCodeForToken({62 serverUrl,63 code,64 redirectUri: window.location.origin + "/#" + DEFAULTS.OAUTH_REDIRECT_PATH, // Add hash65 })66 .then(async (tokens) => {67 await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token);68 69 // Add MCP server to MCPClientService for UI70 const mcpServerUrl = localStorage.getItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL);71 if (mcpServerUrl) {72 const serverName =73 localStorage.getItem(STORAGE_KEYS.MCP_SERVER_NAME) || mcpServerUrl;74 const serverTransport = 75 (localStorage.getItem(STORAGE_KEYS.MCP_SERVER_TRANSPORT) as MCPServerConfig['transport']) || DEFAULTS.MCP_TRANSPORT;76 77 const serverConfig = {78 id: `server_${Date.now()}`,79 name: serverName,80 url: mcpServerUrl,81 enabled: true,82 transport: serverTransport,83 auth: {84 type: "bearer" as const,85 token: tokens.access_token,86 },87 };88 89 let servers: MCPServerConfig[] = [];90 try {91 const stored = localStorage.getItem(STORAGE_KEYS.MCP_SERVERS);92 if (stored) servers = JSON.parse(stored);93 } catch {}94 95 const exists = servers.some((s: MCPServerConfig) => s.url === mcpServerUrl);96 if (!exists) {97 servers.push(serverConfig);98 localStorage.setItem(STORAGE_KEYS.MCP_SERVERS, JSON.stringify(servers));99 }100 101 // Clear temp values102 localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_NAME);103 localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_TRANSPORT);104 localStorage.removeItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL);105 }106 107 // Clear OAuth state108 localStorage.removeItem('oauth_state');109 110 setStatus("Authorization successful! Redirecting...");111 if (onSuccess) onSuccess(tokens);112 113 // Use React Router navigation instead of window.location.replace114 setTimeout(() => {115 navigate("/", { replace: true });116 }, 1000);117 })118 .catch((err) => {119 setStatus("OAuth token exchange failed: " + err.message);120 if (onError) onError(err);121 // Clear OAuth state on error122 localStorage.removeItem('oauth_state');123 });124 } else {125 setStatus("Missing authorization code in callback URL.");126 if (onError) onError(new Error("Missing authorization code"));127 }128 }, [serverUrl, onSuccess, onError, navigate]);129 130 return (131 <div className="flex items-center justify-center min-h-screen">132 <div className="text-center">133 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>134 <p>{status}</p>135 </div>136 </div>137 );138};139 140export default OAuthCallback;