TradingAgents/frontend/app/auth/callback/page.tsx

126 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* OAuth Callback Handler Page
* Handles the redirect from Google OAuth and exchanges code for token
*/
"use client";
import { useEffect, useState, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useAuth } from "@/contexts/auth-context";
function AuthCallbackContent() {
const router = useRouter();
const searchParams = useSearchParams();
const { setAuthFromCallback } = useAuth();
const [error, setError] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(true);
useEffect(() => {
const handleCallback = async () => {
// Check if we have a token directly (backend-handled flow)
const token = searchParams.get("token");
if (token) {
setAuthFromCallback(token);
router.replace("/");
return;
}
// Check if we have a code (frontend-handled flow)
const code = searchParams.get("code");
if (!code) {
const errorParam = searchParams.get("error");
setError(errorParam || "No authorization code received");
setIsProcessing(false);
return;
}
try {
// Exchange code for token via backend
const redirectUri = `${window.location.origin}/api/auth/callback/google`;
const response = await fetch("/api/auth/google/token", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
const errorData = await response.json();
// Handle various error response formats
const errorMessage =
typeof errorData.detail === 'string' ? errorData.detail :
typeof errorData.error === 'string' ? errorData.error :
errorData.message ||
JSON.stringify(errorData) ||
"Failed to exchange token";
throw new Error(errorMessage);
}
const data = await response.json();
setAuthFromCallback(data.access_token);
router.replace("/");
} catch (err: any) {
console.error("Auth callback error:", err);
const msg = err.message || "Authentication failed";
setError(typeof msg === 'string' ? msg : "Authentication failed");
setIsProcessing(false);
}
};
handleCallback();
}, [searchParams, setAuthFromCallback, router]);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center p-8 max-w-md">
<div className="text-red-500 text-5xl mb-4"></div>
<h1 className="text-2xl font-bold mb-2"></h1>
<p className="text-muted-foreground mb-4">{error}</p>
<button
onClick={() => router.replace("/")}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:opacity-90"
>
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-lg font-medium">...</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
);
}
function LoadingFallback() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-lg font-medium">...</p>
</div>
</div>
);
}
export default function AuthCallbackPage() {
return (
<Suspense fallback={<LoadingFallback />}>
<AuthCallbackContent />
</Suspense>
);
}