video-flow-b/lib/auth.ts
2025-09-20 21:27:50 +08:00

671 lines
16 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 = '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登录流程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}/api/auth/google/callback`;
} else if (isDevEnv) {
redirectUri = 'https://www.movieflow.net/api/auth/google/callback'; // 指向正确的API端点
} else if (isProdEnv) {
redirectUri = 'https://www.movieflow.ai/api/auth/google/callback'; // 指向正确的API端点
} else {
// 默认使用生产环境
redirectUri = 'https://www.movieflow.ai/api/auth/google/callback'; // 指向正确的API端点
}
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 authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${authParams.toString()}`;
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;
} 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;
}
};