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>;
|
loading: (message: string) => ReturnType<typeof toast.promise>;
|
||||||
dismiss: () => void;
|
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 {
|
export interface GoogleLoginRequest {
|
||||||
idToken: string;
|
idToken: string;
|
||||||
action?: 'login' | 'register' | 'auto';
|
action?: 'login' | 'register' | 'auto';
|
||||||
|
inviteCode?: string; // 邀请码支持
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google 账户绑定请求参数
|
// Google 账户绑定请求参数
|
||||||
|
|||||||
@ -38,44 +38,54 @@ export default function OAuthCallback() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证state参数防止CSRF攻击
|
// 解析state参数获取邀请码等信息
|
||||||
const savedStateStr = sessionStorage.getItem("google_oauth_state");
|
let stateData: any = {};
|
||||||
if (!savedStateStr) {
|
try {
|
||||||
setStatus("error");
|
stateData = JSON.parse(params.state);
|
||||||
setMessage("OAuth state not found. Please try again.");
|
} catch (e) {
|
||||||
return;
|
console.warn('无法解析state参数:', params.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedState: OAuthState = JSON.parse(savedStateStr);
|
console.log('开始处理Google OAuth回调, code:', params.code?.substring(0, 20) + '...');
|
||||||
if (savedState.state !== params.state) {
|
console.log('State数据:', stateData);
|
||||||
setStatus("error");
|
|
||||||
setMessage("Invalid OAuth state. Possible CSRF attack detected.");
|
// 调用后端处理授权码
|
||||||
return;
|
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) {
|
} catch (error: any) {
|
||||||
console.error("OAuth callback error:", error);
|
console.error("OAuth callback error:", error);
|
||||||
|
|
||||||
|
|||||||
@ -71,6 +71,8 @@ export default function Login() {
|
|||||||
setFormError("Authentication failed, please try again.");
|
setFormError("Authentication failed, please try again.");
|
||||||
} else if (error === "oauth_callback_incomplete") {
|
} else if (error === "oauth_callback_incomplete") {
|
||||||
setFormError("OAuth callback processing is incomplete. Please use the Google login button below.");
|
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]);
|
}, [searchParams]);
|
||||||
@ -79,8 +81,12 @@ export default function Login() {
|
|||||||
try {
|
try {
|
||||||
setGoogleLoading(true);
|
setGoogleLoading(true);
|
||||||
setFormError("");
|
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) {
|
} catch (error: any) {
|
||||||
console.error("Google sign-in error:", error);
|
console.error("Google sign-in error:", error);
|
||||||
setFormError(error.message || "Google sign-in failed, please try again");
|
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 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 {
|
try {
|
||||||
// 获取授权URL
|
console.log('开始Google登录流程(Medium风格)...');
|
||||||
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/authorize`, {
|
|
||||||
method: 'GET',
|
// 生成随机nonce用于安全验证
|
||||||
headers: {
|
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||||||
'Accept': 'application/json',
|
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
// 构建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) {
|
console.log('跳转到Google授权页面:', authUrl);
|
||||||
// 保存state参数用于CSRF防护
|
|
||||||
const oauthState: OAuthState = {
|
// 保存state到sessionStorage用于验证
|
||||||
state: data.data.state,
|
sessionStorage.setItem('google_oauth_state', JSON.stringify({
|
||||||
timestamp: Date.now(),
|
nonce: nonce,
|
||||||
redirectUrl: window.location.pathname
|
timestamp: Date.now(),
|
||||||
};
|
inviteCode: inviteCode || ''
|
||||||
sessionStorage.setItem('google_oauth_state', JSON.stringify(oauthState));
|
}));
|
||||||
|
|
||||||
|
// 直接在当前页面跳转到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) {
|
} catch (error) {
|
||||||
console.error('Google OAuth initialization failed:', error);
|
console.error('Google登录跳转失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -225,12 +301,14 @@ export const signInWithGoogle = async () => {
|
|||||||
* Google ID Token 登录
|
* Google ID Token 登录
|
||||||
* @param idToken Google ID Token
|
* @param idToken Google ID Token
|
||||||
* @param action 操作类型:login | register | auto
|
* @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 {
|
try {
|
||||||
const requestData: GoogleLoginRequest = {
|
const requestData: GoogleLoginRequest = {
|
||||||
idToken,
|
idToken,
|
||||||
action
|
action,
|
||||||
|
inviteCode
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/login`, {
|
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/login`, {
|
||||||
|
|||||||
@ -46,8 +46,24 @@ const nextConfig = {
|
|||||||
async rewrites() {
|
async rewrites() {
|
||||||
// 使用环境变量,如果没有则使用默认值
|
// 使用环境变量,如果没有则使用默认值
|
||||||
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
|
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'
|
// const BASE_URL = 'http://192.168.120.5:8000'
|
||||||
return [
|
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*',
|
source: '/api/proxy/:path*',
|
||||||
destination: `${BASE_URL}/:path*`,
|
destination: `${BASE_URL}/:path*`,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user