forked from 77media/video-flow
702 lines
18 KiB
TypeScript
702 lines
18 KiB
TypeScript
|
||
import type {
|
||
BaseResponse,
|
||
GoogleAuthorizeResponse,
|
||
GoogleLoginRequest,
|
||
GoogleLoginSuccessResponse,
|
||
EmailConflictData,
|
||
OAuthState
|
||
} from '@/app/types/google-oauth';
|
||
|
||
// API配置
|
||
//const JAVA_BASE_URL = 'http://192.168.120.36:8080';
|
||
const JAVA_BASE_URL = process.env.NEXT_PUBLIC_JAVA_URL || 'https://77.app.java.auth.qikongjian.com';
|
||
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
|
||
|
||
// Token存储键
|
||
const TOKEN_KEY = 'token';
|
||
const USER_KEY = 'currentUser';
|
||
|
||
/**
|
||
* 注册用户响应数据
|
||
*/
|
||
type RegisterUserData = {
|
||
user_id: string;
|
||
email: string;
|
||
name: string;
|
||
invite_code: string;
|
||
};
|
||
|
||
/**
|
||
* 注册用户API响应
|
||
*/
|
||
type RegisterUserResponse = {
|
||
code: number;
|
||
message: string;
|
||
data: RegisterUserData;
|
||
successful: boolean;
|
||
};
|
||
|
||
/**
|
||
* 登录用户
|
||
*/
|
||
export const loginUser = async (email: string, password: string) => {
|
||
try {
|
||
const response = await fetch(`${JAVA_BASE_URL}/api/user/login`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
username: email,
|
||
password: password,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if(!data.success){
|
||
throw new Error(data.message)
|
||
}
|
||
// 保存token到本地存储
|
||
setToken(data.token);
|
||
|
||
const user = await getUserProfile();
|
||
|
||
return user;
|
||
} catch (error) {
|
||
console.error('Login failed:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取token
|
||
*/
|
||
export const getToken = (): string | null => {
|
||
if (typeof window === 'undefined') return null;
|
||
return localStorage.getItem(TOKEN_KEY);
|
||
};
|
||
|
||
/**
|
||
* 设置token
|
||
*/
|
||
export const setToken = (token: string) => {
|
||
if (typeof window === 'undefined') return;
|
||
localStorage.setItem(TOKEN_KEY, token);
|
||
};
|
||
|
||
/**
|
||
* 获取当前用户
|
||
*/
|
||
export const getCurrentUser = () => {
|
||
if (typeof window === 'undefined') return null;
|
||
|
||
const userJson = localStorage.getItem(USER_KEY);
|
||
if (!userJson) return null;
|
||
|
||
try {
|
||
return JSON.parse(userJson);
|
||
} catch (error) {
|
||
console.error('Failed to parse user data from storage', error);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 设置用户信息
|
||
*/
|
||
export const setUser = (user: any) => {
|
||
if (typeof window === 'undefined') return;
|
||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||
};
|
||
|
||
/**
|
||
* 清除认证数据
|
||
*/
|
||
export const clearAuthData = () => {
|
||
if (typeof window === 'undefined') return;
|
||
localStorage.removeItem(TOKEN_KEY);
|
||
localStorage.removeItem(USER_KEY);
|
||
};
|
||
|
||
/**
|
||
* 用户登出
|
||
*/
|
||
export const logoutUser = () => {
|
||
clearAuthData();
|
||
window.location.href = '/login';
|
||
};
|
||
|
||
/**
|
||
* 检查是否已登录
|
||
*/
|
||
export const isAuthenticated = (): boolean => {
|
||
return !!getToken();
|
||
};
|
||
|
||
/**
|
||
* 创建带有token的请求头
|
||
*/
|
||
export const getAuthHeaders = () => {
|
||
const token = getToken();
|
||
return {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
...(token && { 'X-EASE-ADMIN-TOKEN': token }),
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 带有自动token处理的fetch封装
|
||
*/
|
||
export const authFetch = async (url: string, options: RequestInit = {}) => {
|
||
const token = getToken();
|
||
|
||
if (!token) {
|
||
throw new Error('No token available');
|
||
}
|
||
|
||
const headers = {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
'X-EASE-ADMIN-TOKEN': token,
|
||
...options.headers,
|
||
};
|
||
|
||
const response = await fetch(url, {
|
||
...options,
|
||
headers,
|
||
});
|
||
|
||
// 检查是否token过期
|
||
if (response.status === 401) {
|
||
const data = await response.json();
|
||
if (data.code === '401') {
|
||
// Token过期,清除本地数据并重定向到登录页
|
||
clearAuthData();
|
||
window.location.href = '/login';
|
||
throw new Error('Token expired');
|
||
}
|
||
}
|
||
|
||
return response;
|
||
};
|
||
|
||
// Google OAuth相关函数
|
||
|
||
// Google Client ID - 从环境变量获取
|
||
const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com';
|
||
|
||
/**
|
||
* 初始化Google GSI SDK
|
||
*/
|
||
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 {
|
||
console.log('开始Google登录流程,使用环境变量配置...');
|
||
|
||
// 从 sessionStorage 获取邀请码(如果没有传入的话)
|
||
let finalInviteCode = inviteCode;
|
||
if (!finalInviteCode) {
|
||
try {
|
||
const sessionInviteCode = sessionStorage.getItem("inviteCode");
|
||
if (sessionInviteCode) {
|
||
finalInviteCode = sessionInviteCode;
|
||
console.log('从 sessionStorage 获取到邀请码:', finalInviteCode);
|
||
}
|
||
} catch (e) {
|
||
console.warn('无法从 sessionStorage 获取邀请码:', e);
|
||
}
|
||
}
|
||
|
||
// 从环境变量获取配置
|
||
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com';
|
||
const javaBaseUrl = process.env.NEXT_PUBLIC_JAVA_URL || 'https://auth.test.movieflow.ai';
|
||
const redirectUri = process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI || `${javaBaseUrl}/api/auth/google/callback`;
|
||
|
||
// 生成随机nonce用于安全验证
|
||
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||
.map(b => b.toString(16).padStart(2, '0')).join('');
|
||
|
||
// 构建state参数(包含邀请码等信息)
|
||
const stateData = {
|
||
inviteCode: finalInviteCode || '',
|
||
timestamp: Date.now(),
|
||
origin: window.location.pathname + window.location.search,
|
||
nonce: nonce
|
||
};
|
||
|
||
console.log('使用的配置:', {
|
||
clientId,
|
||
javaBaseUrl,
|
||
redirectUri,
|
||
envClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
|
||
envJavaUrl: process.env.NEXT_PUBLIC_JAVA_URL,
|
||
envRedirectUri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI
|
||
});
|
||
|
||
// 详细的调试日志
|
||
console.log('🔍 Google OAuth 调试信息:');
|
||
console.log(' - 当前域名:', window.location.hostname);
|
||
console.log(' - 当前协议:', window.location.protocol);
|
||
console.log(' - 当前端口:', window.location.port);
|
||
console.log(' - 完整 origin:', window.location.origin);
|
||
console.log(' - 环境变量 NEXT_PUBLIC_GOOGLE_REDIRECT_URI:', process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI);
|
||
console.log(' - 最终使用的 redirect_uri:', redirectUri);
|
||
console.log(' - Google Client ID:', GOOGLE_CLIENT_ID);
|
||
|
||
// 构建Google OAuth2授权URL
|
||
const authParams = new URLSearchParams({
|
||
access_type: 'online',
|
||
client_id: clientId,
|
||
nonce: nonce,
|
||
redirect_uri: redirectUri,
|
||
response_type: 'code', // 使用授权码模式
|
||
scope: 'email openid profile',
|
||
state: JSON.stringify(stateData),
|
||
prompt: 'select_account' // 总是显示账号选择
|
||
});
|
||
|
||
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${authParams.toString()}`;
|
||
|
||
console.log('跳转到Google授权页面:', authUrl);
|
||
console.log('🔍 调试信息 - 授权URL中的redirect_uri:', authParams.get('redirect_uri'));
|
||
console.log('🔍 调试信息 - 当前页面域名:', window.location.origin);
|
||
console.log('📤 发送给 Google 的完整授权 URL:');
|
||
console.log(authUrl);
|
||
console.log('📋 URL 参数解析:');
|
||
authParams.forEach((value, key) => {
|
||
if (key === 'state') {
|
||
console.log(` - ${key}: ${JSON.stringify(JSON.parse(value), null, 2)}`);
|
||
} else {
|
||
console.log(` - ${key}: ${value}`);
|
||
}
|
||
});
|
||
|
||
// 保存state到sessionStorage用于验证
|
||
sessionStorage.setItem('google_oauth_state', JSON.stringify({
|
||
nonce: nonce,
|
||
timestamp: Date.now(),
|
||
inviteCode: finalInviteCode || ''
|
||
}));
|
||
|
||
// 直接在当前页面跳转到Google
|
||
window.location.href = authUrl;
|
||
|
||
} catch (error) {
|
||
console.error('Google登录跳转失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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', inviteCode?: string) => {
|
||
try {
|
||
const requestData: GoogleLoginRequest = {
|
||
idToken,
|
||
action,
|
||
inviteCode
|
||
};
|
||
|
||
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/login`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(requestData),
|
||
});
|
||
|
||
const data: BaseResponse<GoogleLoginSuccessResponse> = await response.json();
|
||
|
||
if (response.status === 409) {
|
||
// 邮箱冲突处理
|
||
const conflictData = data as unknown as BaseResponse<EmailConflictData>;
|
||
throw {
|
||
type: 'EMAIL_CONFLICT',
|
||
status: 409,
|
||
data: conflictData.data,
|
||
message: data.message || 'Email already exists'
|
||
};
|
||
}
|
||
|
||
if (!data.success || !data.data) {
|
||
throw new Error(data.message || 'Google login failed');
|
||
}
|
||
|
||
// 保存token和用户信息
|
||
setToken(data.data.token);
|
||
setUser(data.data.userInfo);
|
||
|
||
return data.data.userInfo;
|
||
} catch (error) {
|
||
console.error('Google token login failed:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 绑定Google账户到当前用户
|
||
* @param bindToken 绑定令牌
|
||
* @param idToken Google ID Token (可选)
|
||
*/
|
||
export const bindGoogleAccount = async (bindToken: string, idToken?: string) => {
|
||
try {
|
||
const token = getToken();
|
||
if (!token) {
|
||
throw new Error('User not authenticated');
|
||
}
|
||
|
||
const requestData = {
|
||
bindToken,
|
||
idToken,
|
||
confirm: true
|
||
};
|
||
|
||
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/bind`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify(requestData),
|
||
});
|
||
|
||
const data: BaseResponse<{ token: string; message: string }> = await response.json();
|
||
|
||
if (!data.success || !data.data) {
|
||
throw new Error(data.message || 'Account binding failed');
|
||
}
|
||
|
||
// 更新token
|
||
setToken(data.data.token);
|
||
|
||
return data.data;
|
||
} catch (error) {
|
||
console.error('Google account binding failed:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取Google账户绑定状态
|
||
*/
|
||
export const getGoogleBindStatus = async () => {
|
||
try {
|
||
const token = getToken();
|
||
if (!token) {
|
||
throw new Error('User not authenticated');
|
||
}
|
||
|
||
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/status`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
const data: BaseResponse<{ isBound: boolean; userId: string }> = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.message || 'Failed to get bind status');
|
||
}
|
||
|
||
return data.data;
|
||
} catch (error) {
|
||
console.error('Get Google bind status failed:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Generates and stores a state parameter for OAuth to prevent CSRF attacks
|
||
*/
|
||
export const generateOAuthState = () => {
|
||
if (typeof window === 'undefined') return '';
|
||
|
||
// Generate a random string for state
|
||
const state = Math.random().toString(36).substring(2, 15);
|
||
|
||
// Store the state in session storage to validate later
|
||
sessionStorage.setItem('oauthState', state);
|
||
|
||
return state;
|
||
};
|
||
|
||
/**
|
||
* Validates the state parameter returned from OAuth to prevent CSRF attacks
|
||
*/
|
||
export const validateOAuthState = (state: string): boolean => {
|
||
if (typeof window === 'undefined') return false;
|
||
|
||
const storedState = sessionStorage.getItem('oauthState');
|
||
|
||
// Clean up the stored state regardless of validity
|
||
sessionStorage.removeItem('oauthState');
|
||
|
||
// Validate that the returned state matches what we stored
|
||
return state === storedState;
|
||
};
|
||
|
||
/**
|
||
* 获取用户信息
|
||
* @returns {Promise<any>} 用户信息对象
|
||
*/
|
||
export const getUserProfile = async (): Promise<any> => {
|
||
// const t = {
|
||
// id: 'fcb6768b6f49449387e6617f75baabc0',
|
||
// userId: 'fcb6768b6f49449387e6617f75baabc0',
|
||
// username: 'gxy',
|
||
// name: 'gxy',
|
||
// email: 'moviflow66@test.com',
|
||
// role: 'USER',
|
||
// isActive: 1,
|
||
// authType: 'LOCAL',
|
||
// lastLogin: new Date(),
|
||
// }
|
||
// setUser(t);
|
||
// return t;
|
||
try {
|
||
const token = getToken();
|
||
if (!token) {
|
||
throw new Error('No token available');
|
||
}
|
||
|
||
const response = await fetch(`${BASE_URL}/auth/profile`, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(response.status.toString());
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.code === 0 && data.successful) {
|
||
// 更新本地存储的用户信息
|
||
const userInfo = {
|
||
id: data.data.user_id,
|
||
userId: data.data.user_id,
|
||
username: data.data.username,
|
||
name: data.data.name,
|
||
email: data.data.email,
|
||
role: data.data.role,
|
||
isActive: data.data.is_active,
|
||
authType: data.data.auth_type,
|
||
lastLogin: data.data.last_login,
|
||
};
|
||
|
||
setUser(userInfo);
|
||
return userInfo;
|
||
} else {
|
||
throw new Error(data.message || 'Failed to get user profile');
|
||
}
|
||
} catch (error) {
|
||
console.error('Get user profile failed:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 刷新用户信息
|
||
* @returns {Promise<any>} 最新的用户信息
|
||
*/
|
||
export const refreshUserProfile = async (): Promise<any> => {
|
||
try {
|
||
const profile = await getUserProfile();
|
||
return profile;
|
||
} catch (error) {
|
||
console.error('Refresh user profile failed:', error);
|
||
// 如果刷新失败,返回本地存储的用户信息
|
||
return getCurrentUser();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 检查用户权限
|
||
* @param {string} requiredRole 需要的角色权限
|
||
* @returns {boolean} 是否有权限
|
||
*/
|
||
export const checkUserPermission = (requiredRole: string): boolean => {
|
||
const user = getCurrentUser();
|
||
if (!user || !user.role) {
|
||
return false;
|
||
}
|
||
|
||
// 角色权限等级
|
||
const roleHierarchy = {
|
||
'ADMIN': 3,
|
||
'MODERATOR': 2,
|
||
'USER': 1,
|
||
};
|
||
|
||
const userRoleLevel = roleHierarchy[user.role as keyof typeof roleHierarchy] || 0;
|
||
const requiredRoleLevel = roleHierarchy[requiredRole as keyof typeof roleHierarchy] || 0;
|
||
|
||
return userRoleLevel >= requiredRoleLevel;
|
||
};
|
||
|
||
/**
|
||
* 检查用户是否激活
|
||
* @returns {boolean} 用户是否激活
|
||
*/
|
||
export const isUserActive = (): boolean => {
|
||
const user = getCurrentUser();
|
||
return user?.isActive === 1;
|
||
};
|
||
/**
|
||
* 用户注册
|
||
* @param {Object} params - 注册参数
|
||
* @param {string} params.userName - 用户名
|
||
* @param {string} params.password - 密码
|
||
* @param {string} params.email - 邮箱
|
||
* @param {string} [params.inviteCode] - 邀请码(可选)
|
||
* @returns {Promise<any>} 注册结果
|
||
* @throws {Error} 注册失败时抛出异常
|
||
*/
|
||
export const registerUser = async ({
|
||
userName,
|
||
password,
|
||
email,
|
||
inviteCode,
|
||
}: {
|
||
userName: string;
|
||
password: string;
|
||
email: string;
|
||
inviteCode?: string;
|
||
}): Promise<any> => {
|
||
try {
|
||
const response = await fetch(`${BASE_URL}/api/user/register`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
userName,
|
||
password,
|
||
email,
|
||
inviteCode,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
console.log('data', data)
|
||
if(!data.success){
|
||
throw new Error(data.message||data.msg)
|
||
}
|
||
return data as {
|
||
success: boolean;
|
||
};
|
||
} catch (error) {
|
||
console.error('Register failed:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
export const registerUserWithInvite = async ({
|
||
password,
|
||
email,
|
||
invite_code,
|
||
}: {
|
||
password: string;
|
||
email: string;
|
||
invite_code?: string;
|
||
}): Promise<RegisterUserResponse> => {
|
||
try {
|
||
const response = await fetch(`${BASE_URL}/api/user_fission/register_with_invite`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
password,
|
||
email,
|
||
invite_code,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if(!data.successful){
|
||
throw new Error(data.message || '注册失败');
|
||
}
|
||
return data as RegisterUserResponse;
|
||
} catch (error) {
|
||
console.error('Register with invite failed:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 发送验证链接
|
||
* @param {string} email - 邮箱
|
||
* @returns {Promise<any>} 发送验证链接结果
|
||
*/
|
||
export const sendVerificationLink = async (email: string) => {
|
||
try {
|
||
const response = await fetch(`${BASE_URL}/api/user/sendVerificationLink?email=${email}`);
|
||
const data = await response.json();
|
||
if(!data.success){
|
||
throw new Error(data.message||data.msg)
|
||
}
|
||
return data as {
|
||
success: boolean;
|
||
};
|
||
} catch (error) {
|
||
console.error('Send verification link failed:', error);
|
||
throw error;
|
||
}
|
||
}; |