updates google login

This commit is contained in:
Zixin Zhou 2025-09-20 15:37:31 +08:00
parent 9bcbe6b057
commit 5b7dc2fe47
6 changed files with 204 additions and 64 deletions

29
app/types/global.d.ts vendored
View File

@ -12,5 +12,34 @@ declare global {
loading: (message: string) => ReturnType<typeof toast.promise>;
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;
};
};
};
}
}

View File

@ -48,6 +48,7 @@ export interface EmailConflictData {
export interface GoogleLoginRequest {
idToken: string;
action?: 'login' | 'register' | 'auto';
inviteCode?: string; // 邀请码支持
}
// Google 账户绑定请求参数

View File

@ -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);

View File

@ -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");

View File

@ -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<void> => {
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<void> => {
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授权URLMedium风格参数
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<GoogleAuthorizeResponse> = await response.json();
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${authParams.toString()}`;
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));
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;
// 重定向到Google授权页面
window.location.href = data.data.authUrl;
} else {
throw new Error(data.message || 'Failed to get authorization URL');
}
} 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`, {

View File

@ -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*`,