2025-09-23 15:25:49 +08:00

326 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { CheckCircle, XCircle, Loader2, AlertTriangle } from "lucide-react";
import type { OAuthCallbackParams } from "@/app/types/google-oauth";
// 根据接口文档定义响应类型
interface GoogleOAuthResponse {
success: boolean;
data: {
token: string;
user: {
userId: string;
userName: string;
name: string;
email: string;
authType: "GOOGLE";
avatar: string;
isNewUser: boolean;
};
message: string;
};
message?: string;
}
export default function OAuthCallback() {
const router = useRouter();
const searchParams = useSearchParams();
const [status, setStatus] = useState<"loading" | "success" | "error" | "conflict">("loading");
const [message, setMessage] = useState("");
const [conflictData, setConflictData] = useState<any>(null);
useEffect(() => {
const handleOAuthCallback = async () => {
try {
console.log('🎯 Google OAuth 回调页面开始处理...');
console.log('📍 回调页面调试信息:');
console.log(' - 完整回调 URL:', window.location.href);
console.log(' - 回调域名:', window.location.hostname);
console.log(' - 回调协议:', window.location.protocol);
console.log(' - 回调路径:', window.location.pathname);
console.log(' - URL 查询参数:', window.location.search);
// 获取URL参数
const params: OAuthCallbackParams = {
code: searchParams.get("code") || undefined,
state: searchParams.get("state") || undefined,
error: searchParams.get("error") || undefined,
error_description: searchParams.get("error_description") || undefined,
};
console.log('📦 获取到的URL参数:', params);
// 检查是否有错误
if (params.error) {
console.error('OAuth错误:', params.error, params.error_description);
setStatus("error");
setMessage(params.error_description || `OAuth error: ${params.error}`);
return;
}
// 验证必需参数
if (!params.code || !params.state) {
console.error('缺少必需的OAuth参数:', { code: !!params.code, state: !!params.state });
setStatus("error");
setMessage("Missing required OAuth parameters");
return;
}
// 解析state参数获取邀请码等信息
let stateData: any = {};
try {
stateData = JSON.parse(params.state);
console.log('解析后的State数据:', stateData);
} catch (e) {
console.warn('无法解析state参数:', params.state, e);
}
// 从 sessionStorage 获取邀请码
let inviteCode: string | undefined = undefined;
try {
const sessionInviteCode = sessionStorage.getItem("inviteCode");
if (sessionInviteCode) {
inviteCode = sessionInviteCode;
console.log('从 sessionStorage 获取到邀请码:', inviteCode);
}
} catch (e) {
console.warn('无法从 sessionStorage 获取邀请码:', e);
}
// 优先级sessionStorage > state参数 > undefined
const finalInviteCode = inviteCode || stateData.inviteCode || undefined;
console.log('开始处理Google OAuth回调, code:', params.code?.substring(0, 20) + '...');
console.log('State数据:', stateData);
console.log('最终使用的邀请码:', finalInviteCode);
// 根据 jiekou.md 文档调用统一的 Python OAuth 接口
// 使用 NEXT_PUBLIC_BASE_URL 配置,默认为 https://77.smartvideo.py.qikongjian.com
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://77.smartvideo.py.qikongjian.com';
console.log('🔧 调用 Python OAuth 接口:', baseUrl);
const response = await fetch(`${baseUrl}/api/oauth/google`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
code: params.code,
state: params.state,
invite_code: finalInviteCode || null
})
});
console.log('Python OAuth接口响应状态:', response.status);
const result: GoogleOAuthResponse = await response.json();
if (!response.ok || !result.success) {
console.error('Python OAuth接口处理失败:', result);
// 处理常见错误码
if (result.message?.includes('GOOGLE_TOKEN_EXCHANGE_FAILED')) {
throw new Error('Google authorization failed. Please try again.');
} else if (result.message?.includes('INVALID_ID_TOKEN')) {
throw new Error('Invalid Google token. Please try again.');
} else if (result.message?.includes('UPSTREAM_AUTH_ERROR')) {
throw new Error('Authentication service error. Please try again later.');
}
throw new Error(result.message || 'Google OAuth failed');
}
console.log('Google OAuth成功:', {
userId: result.data?.user?.userId,
email: result.data?.user?.email,
isNewUser: result.data?.user?.isNewUser
});
// 处理成功结果
console.log('Google登录成功:', result);
setStatus("success");
setMessage(result.data.message || "Login successful! Redirecting to dashboard...");
// 根据接口文档的响应格式保存用户信息
const { token, user } = result.data;
// 保存用户信息到localStorage
const userData = {
userId: user.userId,
userName: user.userName,
name: user.name,
email: user.email,
authType: user.authType,
avatar: user.avatar,
isNewUser: user.isNewUser
};
localStorage.setItem('currentUser', JSON.stringify(userData));
if (token) {
localStorage.setItem('token', token);
}
// 2秒后跳转到主页
setTimeout(() => {
const returnUrl = '/movies';
window.location.href = returnUrl;
}, 2000);
} catch (error: any) {
console.error("OAuth callback error:", error);
// 检查是否是网络连接错误
if (error.code === 'ECONNREFUSED' || error.message?.includes('fetch failed')) {
console.error('网络连接失败,可能是后端服务不可用');
setStatus("error");
setMessage('Backend service unavailable. Please try again later.');
return;
}
// 检查是否是 JSON 解析错误
if (error.message?.includes('JSON') || error.name === 'SyntaxError') {
console.error('响应数据解析失败:', error);
setStatus("error");
setMessage('Invalid response format from backend services');
return;
}
// 处理邮箱冲突错误
if (error.type === 'EMAIL_CONFLICT') {
setStatus("conflict");
setMessage(error.message);
setConflictData(error.data);
} else {
setStatus("error");
setMessage(error.message || "OAuth callback processing failed");
}
}
};
handleOAuthCallback();
}, [searchParams, router]);
const handleBindAccount = async () => {
try {
// 这里应该实现账户绑定逻辑
// 需要调用 /api/auth/google/bind 接口
console.log("Account binding not yet implemented");
setMessage("Account binding feature is not yet implemented");
} catch (error: any) {
console.error("Account binding failed:", error);
setMessage(error.message || "Account binding failed");
}
};
const handleReturnToLogin = () => {
router.push("/login");
};
const renderContent = () => {
switch (status) {
case "loading":
return (
<div className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-purple-600 rounded-full blur-lg opacity-30 animate-pulse"></div>
<Loader2 className="h-16 w-16 animate-spin text-cyan-400 relative z-10" />
</div>
<p className="text-lg text-gray-300">Processing OAuth callback...</p>
</div>
);
case "success":
return (
<div className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400 to-purple-600 rounded-full blur-lg opacity-30"></div>
<CheckCircle className="h-16 w-16 text-cyan-400 relative z-10" />
</div>
<h2 className="text-2xl font-semibold bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">
Login Successful
</h2>
<p className="text-gray-300 text-center max-w-md">{message}</p>
</div>
);
case "conflict":
return (
<div className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full blur-lg opacity-30"></div>
<AlertTriangle className="h-16 w-16 text-yellow-400 relative z-10" />
</div>
<h2 className="text-2xl font-semibold bg-gradient-to-r from-yellow-400 to-orange-500 bg-clip-text text-transparent">
Account Conflict
</h2>
<p className="text-gray-300 text-center max-w-md">{message}</p>
{conflictData && (
<div className="text-center space-y-4">
<p className="text-sm text-gray-400">
Email: {conflictData.existingUser?.email}
</p>
<div className="flex gap-3">
<button
onClick={handleBindAccount}
className="px-4 py-2 bg-gradient-to-r from-cyan-400 to-purple-600 text-white rounded-lg hover:from-cyan-500 hover:to-purple-700 transition-all duration-300"
>
Bind Account
</button>
<button
onClick={handleReturnToLogin}
className="px-4 py-2 border border-white/20 text-white rounded-lg hover:bg-white/10 transition-all duration-300"
>
Return to Login
</button>
</div>
</div>
)}
</div>
);
case "error":
return (
<div className="flex flex-col items-center justify-center space-y-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-red-500 to-orange-500 rounded-full blur-lg opacity-30"></div>
<XCircle className="h-16 w-16 text-red-400 relative z-10" />
</div>
<h2 className="text-2xl font-semibold bg-gradient-to-r from-red-500 to-orange-500 bg-clip-text text-transparent">
OAuth Failed
</h2>
<p className="text-gray-300 text-center max-w-md">{message}</p>
<button
onClick={handleReturnToLogin}
className="mt-4 px-6 py-2 bg-gradient-to-r from-cyan-400 to-purple-600 text-white rounded-lg hover:from-cyan-500 hover:to-purple-700 transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105"
>
Return to Login
</button>
</div>
);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-black via-gray-900 to-black px-4">
<div className="bg-gradient-to-br from-gray-900/90 to-black/90 backdrop-blur-sm rounded-2xl shadow-2xl p-8 max-w-md w-full border border-gray-700/50 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-cyan-400/10 to-purple-600/10 pointer-events-none"></div>
<div className="relative z-10">
{status === "loading" && (
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-white mb-2 bg-gradient-to-r from-cyan-400 to-purple-600 bg-clip-text text-transparent">
OAuth Callback
</h1>
<p className="text-gray-300">
Please wait while we process your authentication
</p>
</div>
)}
{renderContent()}
</div>
</div>
</div>
);
}