forked from 77media/video-flow
updates google login
This commit is contained in:
parent
9bcbe6b057
commit
5b7dc2fe47
29
app/types/global.d.ts
vendored
29
app/types/global.d.ts
vendored
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -48,6 +48,7 @@ export interface EmailConflictData {
|
||||
export interface GoogleLoginRequest {
|
||||
idToken: string;
|
||||
action?: 'login' | 'register' | 'auto';
|
||||
inviteCode?: string; // 邀请码支持
|
||||
}
|
||||
|
||||
// Google 账户绑定请求参数
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
130
lib/auth.ts
130
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<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授权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<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`, {
|
||||
|
||||
@ -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*`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user