first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,306 @@
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { getErrorMessage } from "@/lib/store/apis/baseApi";
import { useCompleteOAuthFlowMutation, useLazyGetOAuthConfigStatusQuery } from "@/lib/store/apis/mcpApi";
import { Loader2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
interface OAuth2AuthorizerProps {
open: boolean;
onClose: () => void;
onSuccess: () => void;
onError: (error: string) => void;
authorizeUrl: string;
oauthConfigId: string;
mcpClientId: string;
isPerUserOauth?: boolean;
}
export const OAuth2Authorizer: React.FC<OAuth2AuthorizerProps> = ({
open,
onClose,
onSuccess,
onError,
authorizeUrl,
oauthConfigId,
isPerUserOauth,
}) => {
const [status, setStatus] = useState<"confirm" | "pending" | "polling" | "success" | "failed">(isPerUserOauth ? "confirm" : "pending");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const popupRef = useRef<Window | null>(null);
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
const isCompletingRef = useRef(false);
// RTK Query hooks
const [getOAuthStatus] = useLazyGetOAuthConfigStatusQuery();
const [completeOAuth] = useCompleteOAuthFlowMutation();
// Stop polling
const stopPolling = useCallback(() => {
if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current);
pollIntervalRef.current = null;
}
}, []);
// Handle successful OAuth completion
const handleOAuthComplete = useCallback(async () => {
// Guard against concurrent calls (race between postMessage and polling)
if (isCompletingRef.current) return;
isCompletingRef.current = true;
// Close popup if still open
if (popupRef.current && !popupRef.current.closed) {
popupRef.current.close();
}
// Call complete-oauth endpoint using RTK Query mutation
// Use oauthConfigId instead of mcpClientId for multi-instance support
try {
await completeOAuth(oauthConfigId).unwrap();
setStatus("success");
onSuccess();
setTimeout(() => {
onClose();
}, 1000);
} catch (error) {
const errMsg = getErrorMessage(error);
setStatus("failed");
setErrorMessage(errMsg);
onError(errMsg);
}
}, [oauthConfigId, completeOAuth, onSuccess, onClose, onError]);
// Handle OAuth failure
const handleOAuthFailed = useCallback(
(reason: string) => {
stopPolling();
if (popupRef.current && !popupRef.current.closed) {
popupRef.current.close();
}
setStatus("failed");
setErrorMessage(reason);
onError(reason);
},
[stopPolling, onError],
);
// Check OAuth status (called by postMessage or polling)
const checkOAuthStatus = useCallback(async () => {
try {
const result = await getOAuthStatus(oauthConfigId).unwrap();
if (result.status === "authorized") {
stopPolling();
await handleOAuthComplete();
} else if (result.status === "failed" || result.status === "expired") {
handleOAuthFailed(`Authorization ${result.status}`);
}
} catch (error) {
console.error("Error checking OAuth status:", error);
}
}, [oauthConfigId, getOAuthStatus, stopPolling, handleOAuthComplete, handleOAuthFailed]);
// Poll OAuth status
const startPolling = useCallback(() => {
// Clear any existing interval
if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current);
}
pollIntervalRef.current = setInterval(async () => {
// Check if popup is still open
if (popupRef.current && popupRef.current.closed) {
// Popup closed - check status before assuming cancellation
// (OAuth callback page closes the popup after success)
try {
const result = await getOAuthStatus(oauthConfigId).unwrap();
if (result.status === "authorized") {
stopPolling();
await handleOAuthComplete();
} else if (result.status === "failed" || result.status === "expired") {
stopPolling();
handleOAuthFailed("Authorization failed");
}
// pending or other non-terminal: let polling continue
} catch {
// transient fetch error: let polling continue
}
return;
}
await checkOAuthStatus();
}, 2000); // Poll every 2 seconds
}, [checkOAuthStatus, handleOAuthFailed]);
// Open popup and start polling
const openPopup = useCallback(() => {
// Reset completion guard for each fresh OAuth attempt
isCompletingRef.current = false;
// Close any existing popup
if (popupRef.current && !popupRef.current.closed) {
popupRef.current.close();
}
// Open OAuth popup
const width = 600;
const height = 700;
const left = window.screen.width / 2 - width / 2;
const top = window.screen.height / 2 - height / 2;
popupRef.current = window.open(
authorizeUrl,
"oauth_popup",
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`,
);
setStatus("polling");
// Start polling OAuth status
startPolling();
}, [authorizeUrl, startPolling]);
// Listen for postMessage from OAuth callback popup
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Verify message is from OAuth callback
if (event.data?.type === "oauth_success") {
// Trigger immediate status check; stopPolling is called inside
// checkOAuthStatus only after a confirmed terminal state, so
// transient fetch errors still allow polling to continue.
checkOAuthStatus();
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [checkOAuthStatus]);
// Open popup when dialog opens (skip if waiting for user confirmation)
useEffect(() => {
if (open && status === "pending") {
openPopup();
}
}, [open, status, openPopup]);
// Handle user confirming per-user OAuth test
const handleConfirmPerUserOAuth = () => {
setStatus("pending");
openPopup();
};
// Cleanup on unmount
useEffect(() => {
return () => {
stopPolling();
if (popupRef.current && !popupRef.current.closed) {
popupRef.current.close();
}
};
}, [stopPolling]);
const handleRetry = () => {
setErrorMessage(null);
isCompletingRef.current = false;
if (isPerUserOauth) {
setStatus("confirm");
} else {
setStatus("pending");
openPopup();
}
};
const handleCancel = () => {
stopPolling();
isCompletingRef.current = false;
if (popupRef.current && !popupRef.current.closed) {
popupRef.current.close();
}
onClose();
};
return (
<Dialog open={open}>
<DialogContent className="sm:max-w-md" onPointerDownOutside={(e) => e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>{status === "confirm" ? "Test OAuth Configuration" : "OAuth Authorization"}</DialogTitle>
<DialogDescription>
{status === "confirm" && "A one-time login is needed to verify your OAuth setup."}
{status === "pending" && "Opening authorization window..."}
{status === "polling" && "Waiting for authorization..."}
{status === "success" && "Authorization successful!"}
{status === "failed" && "Authorization failed"}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center justify-center space-y-4">
{status === "confirm" && (
<>
<div className="text-muted-foreground space-y-3 text-sm">
<p>
To set up this MCP server, we need to verify that your OAuth configuration is correct and discover the available tools.
</p>
<p>
You will be asked to log in to the OAuth provider. This is a <strong>one-time test</strong> to confirm the setup works.
Your credentials will <strong>not</strong> be stored or used for any other purpose.
</p>
<p>Once verified, each user will authenticate individually when they use this MCP server.</p>
</div>
<div className="flex w-full justify-end space-x-2">
<Button onClick={handleCancel} variant="outline" data-testid="per-user-oauth-cancel">
Cancel
</Button>
<Button onClick={handleConfirmPerUserOAuth} data-testid="per-user-oauth-confirm">
Continue with Test Login
</Button>
</div>
</>
)}
{status === "polling" && (
<>
<Loader2 className="text-secondary-foreground h-4 w-4 animate-spin" />
<p className="text-muted-foreground text-sm">Please complete authorization in the popup window</p>
</>
)}
{status === "success" && (
<>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
<svg className="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="text-sm text-green-600">MCP server connected successfully!</p>
</>
)}
{status === "failed" && (
<>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<p className="text-sm text-red-600">{errorMessage || "An error occurred"}</p>
<Button onClick={handleRetry} variant="outline">
Retry
</Button>
</>
)}
</div>
{status === "polling" && (
<div className="flex justify-end space-x-2">
<Button onClick={handleCancel} variant="outline">
Cancel
</Button>
</div>
)}
</DialogContent>
</Dialog>
);
};