video-flow-b/lib/auth.ts
2025-09-26 20:03:33 +08:00

702 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(`${JAVA_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;
}
};