forked from 77media/video-flow
336 lines
14 KiB
TypeScript
336 lines
14 KiB
TypeScript
"use client";
|
||
|
||
import React, { useEffect, useState } from "react";
|
||
import { useRouter, useSearchParams } from "next/navigation";
|
||
import { CheckCircle, XCircle, Loader2, AlertTriangle } from "lucide-react";
|
||
import { loginWithGoogleToken } from "@/lib/auth";
|
||
import type { OAuthCallbackParams, OAuthState } from "@/app/types/google-oauth";
|
||
|
||
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);
|
||
|
||
// 直接处理 OAuth 回调(两步流程:Java验证 + Python注册)
|
||
console.log('开始直接处理 OAuth 回调,无需经过 API 路由');
|
||
|
||
// 第一步:调用Java验证接口(只验证不创建用户)
|
||
const javaBaseUrl = 'https://auth.test.movieflow.ai';
|
||
console.log('🔧 调用 Java 验证接口:', javaBaseUrl);
|
||
|
||
const verifyResponse = await fetch(`${javaBaseUrl}/api/auth/google/callback`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
code: params.code, // Google authorization code
|
||
state: params.state, // state参数
|
||
inviteCode: finalInviteCode, // 邀请码
|
||
skipUserCreation: true // 🔑 关键:只验证不创建用户
|
||
})
|
||
});
|
||
|
||
console.log('Java验证接口响应状态:', verifyResponse.status);
|
||
const verifyResult = await verifyResponse.json();
|
||
|
||
if (!verifyResponse.ok || !verifyResult.success) {
|
||
console.error('Java验证接口处理失败:', verifyResult);
|
||
throw new Error(verifyResult.message || 'Google token verification failed');
|
||
}
|
||
|
||
console.log('Google Token验证成功:', {
|
||
email: verifyResult.data?.email,
|
||
name: verifyResult.data?.name
|
||
});
|
||
|
||
// 第二步:调用Python注册接口进行用户创建和积分发放
|
||
const smartvideoBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://77.smartvideo.py.qikongjian.com';
|
||
console.log('🔧 调用 Python 注册接口:', smartvideoBaseUrl);
|
||
|
||
const registerResponse = await fetch(`${smartvideoBaseUrl}/api/user_fission/register_with_invite`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
email: verifyResult.data.email,
|
||
name: verifyResult.data.name,
|
||
auth_type: 'GOOGLE',
|
||
google_user_info: {
|
||
email: verifyResult.data.email,
|
||
name: verifyResult.data.name,
|
||
picture: verifyResult.data.picture || '',
|
||
googleId: verifyResult.data.googleId || verifyResult.data.id || '',
|
||
verified: verifyResult.data.verified || true,
|
||
inviteCode: finalInviteCode
|
||
},
|
||
invite_code: finalInviteCode
|
||
})
|
||
});
|
||
|
||
console.log('Python注册接口响应状态:', registerResponse.status);
|
||
const registerResult = await registerResponse.json();
|
||
|
||
if (!registerResponse.ok || !registerResult.successful) {
|
||
console.error('Python注册接口处理失败:', registerResult);
|
||
throw new Error(registerResult.message || 'User registration failed');
|
||
}
|
||
|
||
console.log('Google OAuth注册成功:', {
|
||
userId: registerResult.data?.user_id,
|
||
email: registerResult.data?.email
|
||
});
|
||
|
||
// 处理成功结果
|
||
console.log('Google登录成功:', registerResult);
|
||
setStatus("success");
|
||
setMessage("Login successful! Redirecting to dashboard...");
|
||
|
||
// 保存用户信息到localStorage
|
||
const userData = {
|
||
userId: registerResult.data.user_id,
|
||
userName: registerResult.data.name,
|
||
name: registerResult.data.name,
|
||
email: registerResult.data.email,
|
||
authType: registerResult.data.auth_type || 'GOOGLE',
|
||
isNewUser: true,
|
||
inviteCode: registerResult.data.invite_code
|
||
};
|
||
|
||
localStorage.setItem('currentUser', JSON.stringify(userData));
|
||
if (registerResult.data.token) {
|
||
localStorage.setItem('token', registerResult.data.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>
|
||
);
|
||
}
|