433 lines
10 KiB
TypeScript
433 lines
10 KiB
TypeScript
|
|
import { useState, useEffect, useMemo } from 'react';
|
|||
|
|
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import { useForm } from 'react-hook-form';
|
|||
|
|
import userService from '../services/user_service';
|
|||
|
|
import { rsaPsw } from '../utils/encryption';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* OAuth回调处理Hook
|
|||
|
|
*/
|
|||
|
|
export const useOAuthCallback = () => {
|
|||
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
|
|||
|
|
const error = searchParams.get('error');
|
|||
|
|
const newSearchParams: URLSearchParams = useMemo(
|
|||
|
|
() => new URLSearchParams(searchParams.toString()),
|
|||
|
|
[searchParams],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (error) {
|
|||
|
|
// 显示错误信息(这里可以集成你的消息提示组件)
|
|||
|
|
console.error('OAuth Error:', error);
|
|||
|
|
setTimeout(() => {
|
|||
|
|
navigate('/login');
|
|||
|
|
newSearchParams.delete('error');
|
|||
|
|
setSearchParams(newSearchParams);
|
|||
|
|
}, 1000);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const auth = searchParams.get('auth');
|
|||
|
|
if (auth) {
|
|||
|
|
// 存储认证信息
|
|||
|
|
localStorage.setItem('token', auth);
|
|||
|
|
newSearchParams.delete('auth');
|
|||
|
|
setSearchParams(newSearchParams);
|
|||
|
|
navigate('/');
|
|||
|
|
}
|
|||
|
|
}, [
|
|||
|
|
error,
|
|||
|
|
searchParams,
|
|||
|
|
newSearchParams,
|
|||
|
|
navigate,
|
|||
|
|
setSearchParams,
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
return searchParams.get('auth');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 认证状态管理Hook
|
|||
|
|
*/
|
|||
|
|
export const useAuth = () => {
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
const location = useLocation();
|
|||
|
|
const [searchParams] = useSearchParams();
|
|||
|
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
|||
|
|
const [userInfo, setUserInfo] = useState<any>(null);
|
|||
|
|
const [token, setToken] = useState<string | null>(null);
|
|||
|
|
const [isLoading, setIsLoading] = useState(true);
|
|||
|
|
|
|||
|
|
// OAuth回调处理
|
|||
|
|
const auth = useOAuthCallback();
|
|||
|
|
|
|||
|
|
// 检查认证状态
|
|||
|
|
const checkAuthStatus = () => {
|
|||
|
|
try {
|
|||
|
|
const storedToken = localStorage.getItem('token');
|
|||
|
|
const storedUserInfo = localStorage.getItem('userInfo');
|
|||
|
|
|
|||
|
|
if (storedToken) {
|
|||
|
|
setToken(storedToken);
|
|||
|
|
setIsAuthenticated(true);
|
|||
|
|
|
|||
|
|
if (storedUserInfo) {
|
|||
|
|
setUserInfo(JSON.parse(storedUserInfo));
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
setToken(null);
|
|||
|
|
setIsAuthenticated(false);
|
|||
|
|
setUserInfo(null);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error checking auth status:', error);
|
|||
|
|
setToken(null);
|
|||
|
|
setIsAuthenticated(false);
|
|||
|
|
setUserInfo(null);
|
|||
|
|
} finally {
|
|||
|
|
setIsLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 登出功能
|
|||
|
|
const logout = () => {
|
|||
|
|
localStorage.removeItem('token');
|
|||
|
|
localStorage.removeItem('userInfo');
|
|||
|
|
setToken(null);
|
|||
|
|
setIsAuthenticated(false);
|
|||
|
|
setUserInfo(null);
|
|||
|
|
navigate('/login');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 更新认证状态
|
|||
|
|
const updateAuthStatus = (newToken: string, newUserInfo: any) => {
|
|||
|
|
setToken(newToken);
|
|||
|
|
setUserInfo(newUserInfo);
|
|||
|
|
setIsAuthenticated(true);
|
|||
|
|
localStorage.setItem('token', newToken);
|
|||
|
|
localStorage.setItem('userInfo', JSON.stringify(newUserInfo));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 带参数重定向到登录页面
|
|||
|
|
const redirectToLogin = (returnUrl?: string) => {
|
|||
|
|
const currentPath = returnUrl || location.pathname + location.search;
|
|||
|
|
if (currentPath !== '/login') {
|
|||
|
|
navigate(`/login?redirect=${encodeURIComponent(currentPath)}`);
|
|||
|
|
} else {
|
|||
|
|
navigate('/login');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 重定向到指定页面或首页
|
|||
|
|
const redirectAfterLogin = () => {
|
|||
|
|
const redirectUrl = searchParams.get('redirect');
|
|||
|
|
if (redirectUrl) {
|
|||
|
|
navigate(decodeURIComponent(redirectUrl));
|
|||
|
|
} else {
|
|||
|
|
navigate('/');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
checkAuthStatus();
|
|||
|
|
}, [auth]);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
isAuthenticated,
|
|||
|
|
userInfo,
|
|||
|
|
token,
|
|||
|
|
isLoading,
|
|||
|
|
checkAuthStatus,
|
|||
|
|
logout,
|
|||
|
|
updateAuthStatus,
|
|||
|
|
redirectToLogin,
|
|||
|
|
redirectAfterLogin,
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export interface LoginFormData {
|
|||
|
|
email: string;
|
|||
|
|
password: string;
|
|||
|
|
rememberEmail?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface RegisterFormData {
|
|||
|
|
email: string;
|
|||
|
|
password: string;
|
|||
|
|
nickname: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 登录Hook
|
|||
|
|
*/
|
|||
|
|
export const useLogin = () => {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
const [searchParams] = useSearchParams();
|
|||
|
|
const [isLoading, setIsLoading] = useState(false);
|
|||
|
|
|
|||
|
|
const login = async (data: LoginFormData) => {
|
|||
|
|
setIsLoading(true);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// RSA加密密码
|
|||
|
|
const encryptedPassword = rsaPsw(data.password);
|
|||
|
|
const response = await userService.login({
|
|||
|
|
email: data.email.trim(),
|
|||
|
|
password: encryptedPassword
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const { data: res = {} } = response;
|
|||
|
|
const { code, message } = res;
|
|||
|
|
|
|||
|
|
if (code === 0) {
|
|||
|
|
// 登录成功,处理token和用户信息
|
|||
|
|
const { data: userData } = res;
|
|||
|
|
const token = userData.access_token;
|
|||
|
|
const userInfo = {
|
|||
|
|
...userData,
|
|||
|
|
avatar: userData.avatar,
|
|||
|
|
name: userData.nickname,
|
|||
|
|
email: userData.email,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 同步更新localStorage
|
|||
|
|
localStorage.setItem('token', token);
|
|||
|
|
localStorage.setItem('userInfo', JSON.stringify(userInfo));
|
|||
|
|
|
|||
|
|
// 直接进行重定向,不依赖React状态更新
|
|||
|
|
const redirectUrl = searchParams.get('redirect');
|
|||
|
|
if (redirectUrl) {
|
|||
|
|
navigate(decodeURIComponent(redirectUrl), { replace: true });
|
|||
|
|
} else {
|
|||
|
|
navigate('/', { replace: true });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { success: true };
|
|||
|
|
} else {
|
|||
|
|
// 登录失败
|
|||
|
|
return {
|
|||
|
|
success: false,
|
|||
|
|
error: message || t('login.loginFailed')
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
} catch (error: any) {
|
|||
|
|
// 处理网络错误或其他异常
|
|||
|
|
return {
|
|||
|
|
success: false,
|
|||
|
|
error: error.message || t('login.networkError')
|
|||
|
|
};
|
|||
|
|
} finally {
|
|||
|
|
setIsLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return { login, isLoading };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 注册Hook
|
|||
|
|
*/
|
|||
|
|
export const useRegister = () => {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [isLoading, setIsLoading] = useState(false);
|
|||
|
|
|
|||
|
|
const register = async (data: RegisterFormData) => {
|
|||
|
|
setIsLoading(true);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// RSA加密密码
|
|||
|
|
const encryptedPassword = rsaPsw(data.password);
|
|||
|
|
|
|||
|
|
const response = await userService.register({
|
|||
|
|
nickname: data.nickname,
|
|||
|
|
email: data.email.trim(),
|
|||
|
|
password: encryptedPassword
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const { data: res = {} } = response;
|
|||
|
|
const { code, message } = res;
|
|||
|
|
|
|||
|
|
if (code === 0) {
|
|||
|
|
// 注册成功
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
email: data.email
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
// 注册失败
|
|||
|
|
return {
|
|||
|
|
success: false,
|
|||
|
|
error: message || t('login.registerFailed')
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
} catch (error: any) {
|
|||
|
|
// 处理网络错误或其他异常
|
|||
|
|
return {
|
|||
|
|
success: false,
|
|||
|
|
error: error.message || t('login.networkError')
|
|||
|
|
};
|
|||
|
|
} finally {
|
|||
|
|
setIsLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return { register, isLoading };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 登录表单管理Hook
|
|||
|
|
*/
|
|||
|
|
export const useLoginForm = () => {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const [tabValue, setTabValue] = useState(0); // 0: 登录, 1: 注册
|
|||
|
|
const [error, setError] = useState('');
|
|||
|
|
|
|||
|
|
// 登录表单
|
|||
|
|
const loginForm = useForm<LoginFormData>({
|
|||
|
|
defaultValues: {
|
|||
|
|
email: '',
|
|||
|
|
password: '',
|
|||
|
|
rememberEmail: false
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 注册表单
|
|||
|
|
const registerForm = useForm<RegisterFormData>({
|
|||
|
|
defaultValues: {
|
|||
|
|
email: '',
|
|||
|
|
password: '',
|
|||
|
|
nickname: ''
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
|||
|
|
setTabValue(newValue);
|
|||
|
|
setError('');
|
|||
|
|
// 清空表单错误
|
|||
|
|
loginForm.clearErrors();
|
|||
|
|
registerForm.clearErrors();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const switchToLogin = (email?: string) => {
|
|||
|
|
setTabValue(0);
|
|||
|
|
setError('');
|
|||
|
|
if (email) {
|
|||
|
|
loginForm.setValue('email', email);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const setFormError = (errorMessage: string) => {
|
|||
|
|
setError(errorMessage);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const clearError = () => {
|
|||
|
|
setError('');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
// 表单状态
|
|||
|
|
tabValue,
|
|||
|
|
setTabValue,
|
|||
|
|
error,
|
|||
|
|
loginForm,
|
|||
|
|
registerForm,
|
|||
|
|
|
|||
|
|
// 表单操作
|
|||
|
|
handleTabChange,
|
|||
|
|
switchToLogin,
|
|||
|
|
setFormError,
|
|||
|
|
clearError,
|
|||
|
|
|
|||
|
|
// 表单验证规则
|
|||
|
|
loginValidation: {
|
|||
|
|
email: {
|
|||
|
|
required: t('login.emailPlaceholder'),
|
|||
|
|
pattern: {
|
|||
|
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|||
|
|
message: t('login.emailInvalid')
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
password: {
|
|||
|
|
required: t('login.passwordPlaceholder'),
|
|||
|
|
minLength: {
|
|||
|
|
value: 6,
|
|||
|
|
message: t('login.passwordMinLength')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
registerValidation: {
|
|||
|
|
nickname: {
|
|||
|
|
required: t('login.nicknamePlaceholder'),
|
|||
|
|
minLength: {
|
|||
|
|
value: 2,
|
|||
|
|
message: t('login.nicknameMinLength')
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
email: {
|
|||
|
|
required: t('login.emailPlaceholder'),
|
|||
|
|
pattern: {
|
|||
|
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|||
|
|
message: t('login.emailInvalid')
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
password: {
|
|||
|
|
required: t('login.passwordPlaceholder'),
|
|||
|
|
minLength: {
|
|||
|
|
value: 8,
|
|||
|
|
message: t('login.passwordMinLength')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 登录页面主Hook - 整合所有登录相关逻辑
|
|||
|
|
*/
|
|||
|
|
export const useLoginPage = () => {
|
|||
|
|
const loginHook = useLogin();
|
|||
|
|
const registerHook = useRegister();
|
|||
|
|
const formHook = useLoginForm();
|
|||
|
|
|
|||
|
|
const handleLogin = async (data: LoginFormData) => {
|
|||
|
|
formHook.clearError();
|
|||
|
|
const result = await loginHook.login(data);
|
|||
|
|
|
|||
|
|
if (!result.success && result.error) {
|
|||
|
|
formHook.setFormError(result.error);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleRegister = async (data: RegisterFormData) => {
|
|||
|
|
formHook.clearError();
|
|||
|
|
const result = await registerHook.register(data);
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
// 注册成功,切换到登录表单并填入邮箱
|
|||
|
|
formHook.registerForm.reset();
|
|||
|
|
formHook.switchToLogin(result.email);
|
|||
|
|
} else if (result.error) {
|
|||
|
|
formHook.setFormError(result.error);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const isSubmitting = loginHook.isLoading || registerHook.isLoading;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
// 表单相关
|
|||
|
|
...formHook,
|
|||
|
|
|
|||
|
|
// 提交处理
|
|||
|
|
handleLogin,
|
|||
|
|
handleRegister,
|
|||
|
|
isSubmitting,
|
|||
|
|
};
|
|||
|
|
};
|