From 5b7dc2fe47bd4b43bf3811881e17e8f982a0fb93 Mon Sep 17 00:00:00 2001 From: Zixin Zhou Date: Sat, 20 Sep 2025 15:37:31 +0800 Subject: [PATCH] updates google login --- app/types/global.d.ts | 29 +++++++ app/types/google-oauth.ts | 1 + app/users/oauth/callback/page.tsx | 78 +++++++++-------- components/pages/login.tsx | 10 ++- lib/auth.ts | 134 +++++++++++++++++++++++------- next.config.js | 16 ++++ 6 files changed, 204 insertions(+), 64 deletions(-) diff --git a/app/types/global.d.ts b/app/types/global.d.ts index 6136c85..17e1311 100644 --- a/app/types/global.d.ts +++ b/app/types/global.d.ts @@ -12,5 +12,34 @@ declare global { loading: (message: string) => ReturnType; dismiss: () => void; }; + // Google GSI API类型声明 + google?: { + accounts: { + id: { + initialize: (config: { + client_id: string; + callback: (response: { credential: string }) => void; + auto_select?: boolean; + cancel_on_tap_outside?: boolean; + }) => void; + prompt: (callback?: (notification: { + isNotDisplayed: () => boolean; + isSkippedMoment: () => boolean; + }) => void) => void; + renderButton: ( + element: HTMLElement, + options: { + theme?: 'outline' | 'filled_blue' | 'filled_black'; + size?: 'large' | 'medium' | 'small'; + text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin'; + shape?: 'rectangular' | 'pill' | 'circle' | 'square'; + logo_alignment?: 'left' | 'center'; + width?: string | number; + locale?: string; + } + ) => void; + }; + }; + }; } } \ No newline at end of file diff --git a/app/types/google-oauth.ts b/app/types/google-oauth.ts index 79e5979..88326b7 100644 --- a/app/types/google-oauth.ts +++ b/app/types/google-oauth.ts @@ -48,6 +48,7 @@ export interface EmailConflictData { export interface GoogleLoginRequest { idToken: string; action?: 'login' | 'register' | 'auto'; + inviteCode?: string; // 邀请码支持 } // Google 账户绑定请求参数 diff --git a/app/users/oauth/callback/page.tsx b/app/users/oauth/callback/page.tsx index ff41b8b..23f1607 100644 --- a/app/users/oauth/callback/page.tsx +++ b/app/users/oauth/callback/page.tsx @@ -38,44 +38,54 @@ export default function OAuthCallback() { return; } - // 验证state参数防止CSRF攻击 - const savedStateStr = sessionStorage.getItem("google_oauth_state"); - if (!savedStateStr) { - setStatus("error"); - setMessage("OAuth state not found. Please try again."); - return; + // 解析state参数获取邀请码等信息 + let stateData: any = {}; + try { + stateData = JSON.parse(params.state); + } catch (e) { + console.warn('无法解析state参数:', params.state); } - const savedState: OAuthState = JSON.parse(savedStateStr); - if (savedState.state !== params.state) { - setStatus("error"); - setMessage("Invalid OAuth state. Possible CSRF attack detected."); - return; + console.log('开始处理Google OAuth回调, code:', params.code?.substring(0, 20) + '...'); + console.log('State数据:', stateData); + + // 调用后端处理授权码 + const response = await fetch('/api/auth/google/callback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + code: params.code, + state: params.state, + inviteCode: stateData.inviteCode || undefined + }) + }); + + const result = await response.json(); + + if (response.ok && result.success) { + console.log('Google登录成功:', result); + setStatus("success"); + setMessage("Login successful! Redirecting to dashboard..."); + + // 保存用户信息到localStorage + if (result.data?.user) { + localStorage.setItem('user', JSON.stringify(result.data.user)); + } + if (result.data?.token) { + localStorage.setItem('token', result.data.token); + } + + // 2秒后跳转到主页 + setTimeout(() => { + const returnUrl = stateData.origin || '/movies'; + window.location.href = returnUrl; + }, 2000); + } else { + throw new Error(result.message || 'Google登录失败'); } - // 检查state是否过期(5分钟) - if (Date.now() - savedState.timestamp > 5 * 60 * 1000) { - setStatus("error"); - setMessage("OAuth session expired. Please try again."); - return; - } - - // 清除保存的state - sessionStorage.removeItem("google_oauth_state"); - - // 这里应该调用后端API处理授权码 - // 但根据当前的实现,我们需要使用Google JavaScript SDK - // 或者等待后端提供处理授权码的接口 - - // 临时处理:重定向到登录页面并显示消息 - setStatus("error"); - setMessage("OAuth callback processing is not yet implemented. Please use the Google login button directly."); - - // 3秒后重定向到登录页面 - setTimeout(() => { - router.push("/login?error=oauth_callback_incomplete"); - }, 3000); - } catch (error: any) { console.error("OAuth callback error:", error); diff --git a/components/pages/login.tsx b/components/pages/login.tsx index 70cb382..fc8738f 100644 --- a/components/pages/login.tsx +++ b/components/pages/login.tsx @@ -71,6 +71,8 @@ export default function Login() { setFormError("Authentication failed, please try again."); } else if (error === "oauth_callback_incomplete") { setFormError("OAuth callback processing is incomplete. Please use the Google login button below."); + } else if (error === "deprecated_oauth_callback") { + setFormError("The old OAuth method is deprecated. Please use the Google login button below for a better experience."); } } }, [searchParams]); @@ -79,8 +81,12 @@ export default function Login() { try { setGoogleLoading(true); setFormError(""); - // signInWithGoogle now returns a promise and may throw errors - await signInWithGoogle(); + + // 获取邀请码(从URL参数或其他来源) + const inviteCode = searchParams?.get("invite") || undefined; + + // 使用Google GSI SDK进行登录 + await signInWithGoogle(inviteCode); } catch (error: any) { console.error("Google sign-in error:", error); setFormError(error.message || "Google sign-in failed, please try again"); diff --git a/lib/auth.ts b/lib/auth.ts index 2887784..0e89469 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -185,38 +185,114 @@ export const authFetch = async (url: string, options: RequestInit = {}) => { // Google OAuth相关函数 +// Google Client ID +const GOOGLE_CLIENT_ID = '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com'; + /** - * Initiates Google OAuth authentication flow using backend API + * 初始化Google GSI SDK */ -export const signInWithGoogle = async () => { +export const initializeGoogleGSI = (): Promise => { + return new Promise((resolve, reject) => { + // 检查是否已经加载 + if (window.google?.accounts?.id) { + resolve(); + return; + } + + // 动态加载Google GSI脚本 + const script = document.createElement('script'); + script.src = 'https://accounts.google.com/gsi/client'; + script.async = true; + script.defer = true; + + script.onload = () => { + // 等待Google API完全加载 + const checkGoogleReady = () => { + if (window.google?.accounts?.id) { + resolve(); + } else { + setTimeout(checkGoogleReady, 100); + } + }; + checkGoogleReady(); + }; + + script.onerror = () => { + reject(new Error('Failed to load Google GSI SDK')); + }; + + document.head.appendChild(script); + }); +}; + +/** + * Medium风格: 页面直接跳转到Google登录 (推荐) + * 参考Medium的实现方式,使用页面跳转 + * @param inviteCode 邀请码(可选) + */ +export const signInWithGoogle = async (inviteCode?: string): Promise => { try { - // 获取授权URL - const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/authorize`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, + console.log('开始Google登录流程(Medium风格)...'); + + // 生成随机nonce用于安全验证 + const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32))) + .map(b => b.toString(16).padStart(2, '0')).join(''); + + // 构建state参数 + const stateData = { + inviteCode: inviteCode || '', + timestamp: Date.now(), + origin: window.location.pathname + window.location.search, + nonce: nonce + }; + + // 根据环境确定正确的redirect_uri + const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; + const isDevEnv = window.location.hostname.includes('movieflow.net'); + const isProdEnv = window.location.hostname.includes('movieflow.ai'); + + let redirectUri; + if (isLocalhost) { + redirectUri = `${window.location.origin}/users/oauth/callback`; + } else if (isDevEnv) { + redirectUri = 'https://www.movieflow.net/api/auth/google/callback'; + } else if (isProdEnv) { + redirectUri = 'https://www.movieflow.ai/api/auth/google/callback'; + } else { + // 默认使用生产环境 + redirectUri = 'https://www.movieflow.ai/api/auth/google/callback'; + } + + console.log('使用的redirect_uri:', redirectUri); + + // 构建Google OAuth2授权URL(Medium风格参数) + const authParams = new URLSearchParams({ + access_type: 'online', + client_id: GOOGLE_CLIENT_ID, + nonce: nonce, + redirect_uri: redirectUri, + response_type: 'code', // 使用授权码模式 + scope: 'email openid profile', + state: JSON.stringify(stateData), + prompt: 'select_account' // 总是显示账号选择 }); - const data: BaseResponse = await response.json(); - - if (data.success && data.data) { - // 保存state参数用于CSRF防护 - const oauthState: OAuthState = { - state: data.data.state, - timestamp: Date.now(), - redirectUrl: window.location.pathname - }; - sessionStorage.setItem('google_oauth_state', JSON.stringify(oauthState)); - - // 重定向到Google授权页面 - window.location.href = data.data.authUrl; - } else { - throw new Error(data.message || 'Failed to get authorization URL'); - } + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${authParams.toString()}`; + + console.log('跳转到Google授权页面:', authUrl); + + // 保存state到sessionStorage用于验证 + sessionStorage.setItem('google_oauth_state', JSON.stringify({ + nonce: nonce, + timestamp: Date.now(), + inviteCode: inviteCode || '' + })); + + // 直接在当前页面跳转到Google (Medium风格) + window.location.href = authUrl; + } catch (error) { - console.error('Google OAuth initialization failed:', error); + console.error('Google登录跳转失败:', error); throw error; } }; @@ -225,12 +301,14 @@ export const signInWithGoogle = async () => { * Google ID Token 登录 * @param idToken Google ID Token * @param action 操作类型:login | register | auto + * @param inviteCode 邀请码(可选) */ -export const loginWithGoogleToken = async (idToken: string, action: 'login' | 'register' | 'auto' = 'auto') => { +export const loginWithGoogleToken = async (idToken: string, action: 'login' | 'register' | 'auto' = 'auto', inviteCode?: string) => { try { const requestData: GoogleLoginRequest = { idToken, - action + action, + inviteCode }; const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/login`, { diff --git a/next.config.js b/next.config.js index b72d9b6..a249a2d 100644 --- a/next.config.js +++ b/next.config.js @@ -46,8 +46,24 @@ const nextConfig = { async rewrites() { // 使用环境变量,如果没有则使用默认值 const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL + // 用户认证后端API + const AUTH_API_URL = process.env.NEXT_PUBLIC_JAVA_URL // const BASE_URL = 'http://192.168.120.5:8000' return [ + // Google OAuth2 回调代理 + { + source: '/api/auth/google/:path*', + destination: `${AUTH_API_URL}/api/auth/google/:path*`, + }, + // 其他认证相关API代理 + { + source: '/api/auth/:path*', + destination: `${AUTH_API_URL}/api/auth/:path*`, + }, + { + source: '/api/user/:path*', + destination: `${AUTH_API_URL}/api/user/:path*`, + }, { source: '/api/proxy/:path*', destination: `${BASE_URL}/:path*`,