feat(auth): implement secure authentication flow with RSA encryption
- Add jsencrypt and js-base64 dependencies for RSA encryption - Create AuthGuard component for route protection - Implement encryption utility for password security - Refactor login page with tabbed interface and form validation - Add login hooks for authentication state management - Update user service to handle encrypted passwords
This commit is contained in:
4
.env
4
.env
@@ -1 +1,5 @@
|
||||
VITE_API_BASE_URL = http://150.158.121.95
|
||||
|
||||
VITE_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
@@ -19,6 +19,8 @@
|
||||
"dayjs": "^1.11.18",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"js-base64": "^3.7.8",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -35,6 +35,12 @@ importers:
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
js-base64:
|
||||
specifier: ^3.7.8
|
||||
version: 3.7.8
|
||||
jsencrypt:
|
||||
specifier: ^3.5.4
|
||||
version: 3.5.4
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -1217,6 +1223,9 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
js-base64@3.7.8:
|
||||
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
||||
|
||||
js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -1228,6 +1237,9 @@ packages:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
|
||||
jsencrypt@3.5.4:
|
||||
resolution: {integrity: sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==}
|
||||
|
||||
jsesc@3.1.0:
|
||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2769,6 +2781,8 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
js-base64@3.7.8: {}
|
||||
|
||||
js-cookie@3.0.5: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -2777,6 +2791,8 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsencrypt@3.5.4: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
@@ -3,8 +3,10 @@ import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { theme } from './theme';
|
||||
import AppRoutes from './routes';
|
||||
import SnackbarProvider from './components/Provider/SnackbarProvider';
|
||||
import AuthGuard from './components/AuthGuard';
|
||||
|
||||
import './locales';
|
||||
import './utils/request'
|
||||
|
||||
/**
|
||||
* 封装MaterialUIApp,将主题、基础样式和路由包裹起来
|
||||
@@ -15,7 +17,9 @@ function MaterialUIApp() {
|
||||
<CssBaseline />
|
||||
<SnackbarProvider>
|
||||
<BrowserRouter>
|
||||
<AuthGuard>
|
||||
<AppRoutes />
|
||||
</AuthGuard>
|
||||
</BrowserRouter>
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
52
src/components/AuthGuard.tsx
Normal file
52
src/components/AuthGuard.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { CircularProgress, Box } from '@mui/material';
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AuthGuard: React.FC<AuthGuardProps> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 直接从localStorage检查token,避免React状态更新的延迟
|
||||
const token = localStorage.getItem('token');
|
||||
const isAuthenticated = !!token;
|
||||
const isLoginPage = location.pathname === '/login';
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && isLoginPage) {
|
||||
// 已登录用户访问登录页,重定向到首页
|
||||
navigate('/', { replace: true });
|
||||
} else if (!isAuthenticated && !isLoginPage) {
|
||||
// 未登录用户访问其他页面,重定向到登录页并保存当前路径
|
||||
const redirectUrl = encodeURIComponent(location.pathname + location.search);
|
||||
navigate(`/login?redirect=${redirectUrl}`, { replace: true });
|
||||
}
|
||||
}, [isAuthenticated, isLoginPage, location, navigate]);
|
||||
|
||||
// 如果是登录页面,直接渲染
|
||||
if (isLoginPage) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// 如果未认证,显示加载状态(实际上会被重定向)
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minHeight="100vh"
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 已认证,渲染子组件
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthGuard;
|
||||
433
src/hooks/login-hooks.ts
Normal file
433
src/hooks/login-hooks.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Box,
|
||||
@@ -14,54 +12,35 @@ import {
|
||||
Toolbar,
|
||||
Card,
|
||||
CardContent,
|
||||
Alert
|
||||
Alert,
|
||||
Tabs,
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||
import userService from '../services/user_service';
|
||||
import { useLoginPage } from '../hooks/login-hooks';
|
||||
|
||||
const Login = () => {
|
||||
const { t } = useTranslation();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [rememberEmail, setRememberEmail] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
const [loginError, setLoginError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
// 表单状态
|
||||
tabValue,
|
||||
setTabValue,
|
||||
error,
|
||||
loginForm,
|
||||
registerForm,
|
||||
|
||||
console.log(t, t('en'), t('login'));
|
||||
// 表单操作
|
||||
handleTabChange,
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// 提交处理
|
||||
handleLogin,
|
||||
handleRegister,
|
||||
isSubmitting,
|
||||
|
||||
const hasEmail = !!email.trim();
|
||||
const hasPassword = !!password.trim();
|
||||
setEmailError(!hasEmail);
|
||||
setPasswordError(!hasPassword);
|
||||
setLoginError('');
|
||||
|
||||
if (!hasEmail || !hasPassword) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await userService.login({ email, password });
|
||||
|
||||
// if (response.code === 0) {
|
||||
// // 登录成功,跳转到主页
|
||||
// navigate('/');
|
||||
// } else {
|
||||
// // 登录失败,显示错误信息
|
||||
// setLoginError(response.message || t('login.loginFailed'));
|
||||
// }
|
||||
} catch (error: any) {
|
||||
// 处理网络错误或其他异常
|
||||
setLoginError(error.message || t('login.networkError'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
// 验证规则
|
||||
loginValidation,
|
||||
registerValidation
|
||||
} = useLoginPage();
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', width: '100vw', bgcolor: 'background.default', display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -96,43 +75,47 @@ const Login = () => {
|
||||
<Typography variant="subtitle1" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||
T-Systems Enterprise RAG Empowerment System
|
||||
</Typography>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ mb: 2 }}>
|
||||
{t('login.login')}
|
||||
<br /> {t('login.emailLabel')} & {t('login.passwordLabel')}
|
||||
</Typography>
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit} noValidate>
|
||||
{loginError && (
|
||||
{/* 标签页 */}
|
||||
<Tabs value={tabValue} onChange={handleTabChange} sx={{ mb: 3 }}>
|
||||
<Tab label={t('login.login')} />
|
||||
<Tab label={t('login.signUp')} />
|
||||
</Tabs>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{loginError}
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 登录表单 */}
|
||||
{tabValue === 0 && (
|
||||
<Box component="form" onSubmit={loginForm.handleSubmit(handleLogin)} noValidate>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ mb: 2 }}>
|
||||
{t('login.loginDescription')}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="email"
|
||||
name="email"
|
||||
id="login-email"
|
||||
type="email"
|
||||
placeholder={t('login.emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
error={emailError}
|
||||
required
|
||||
{...loginForm.register('email', loginValidation.email)}
|
||||
error={!!loginForm.formState.errors.email}
|
||||
helperText={loginForm.formState.errors.email?.message}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="password"
|
||||
name="password"
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
error={passwordError}
|
||||
required
|
||||
{...loginForm.register('password', loginValidation.password)}
|
||||
error={!!loginForm.formState.errors.password}
|
||||
helperText={loginForm.formState.errors.password?.message}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
@@ -140,8 +123,7 @@ const Login = () => {
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={rememberEmail}
|
||||
onChange={(e) => setRememberEmail(e.target.checked)}
|
||||
{...loginForm.register('rememberEmail')}
|
||||
id="rememberEmail"
|
||||
/>
|
||||
}
|
||||
@@ -149,14 +131,82 @@ const Login = () => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button type="submit" variant="contained" fullWidth disabled={isSubmitting}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
disabled={isSubmitting}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{isSubmitting ? 'Processing...' : t('login.login')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 注册表单 */}
|
||||
{tabValue === 1 && (
|
||||
<Box component="form" onSubmit={registerForm.handleSubmit(handleRegister)} noValidate>
|
||||
<Typography variant="h5" fontWeight={700} sx={{ mb: 2 }}>
|
||||
{t('login.registerDescription')}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="register-nickname"
|
||||
placeholder={t('login.nicknamePlaceholder')}
|
||||
{...registerForm.register('nickname', registerValidation.nickname)}
|
||||
error={!!registerForm.formState.errors.nickname}
|
||||
helperText={registerForm.formState.errors.nickname?.message}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="register-email"
|
||||
type="email"
|
||||
placeholder={t('login.emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...registerForm.register('email', registerValidation.email)}
|
||||
error={!!registerForm.formState.errors.email}
|
||||
helperText={registerForm.formState.errors.email?.message}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="register-password"
|
||||
type="password"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
autoComplete="new-password"
|
||||
{...registerForm.register('password', registerValidation.password)}
|
||||
error={!!registerForm.formState.errors.password}
|
||||
helperText={registerForm.formState.errors.password?.message}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
disabled={isSubmitting}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{isSubmitting ? 'Processing...' : t('login.register')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
||||
<Box mt={0.5}>
|
||||
{t('login.signInTip')} <Link href="#">{t('login.signUp')}</Link>.
|
||||
{tabValue === 0 ? (
|
||||
<>
|
||||
{t('login.signInTip')} <Link href="#" onClick={() => setTabValue(1)}>{t('login.signUp')}</Link>.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('login.signUpTip')} <Link href="#" onClick={() => setTabValue(0)}>{t('login.login')}</Link>.
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
@@ -14,7 +14,7 @@ const userService = {
|
||||
},
|
||||
|
||||
// 用户注册
|
||||
register: (data: { email: string; password: string; username?: string }) => {
|
||||
register: (data: { email: string; password: string; nickname?: string }) => {
|
||||
return post(api.register, data);
|
||||
},
|
||||
|
||||
|
||||
36
src/utils/encryption.ts
Normal file
36
src/utils/encryption.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
// RSA公钥
|
||||
let RSA_PUBLIC_KEY = (import.meta.env.VITE_RSA_PUBLIC_KEY as string) || '';
|
||||
|
||||
/**
|
||||
* RSA密码加密函数
|
||||
* @param password 明文密码
|
||||
* @returns 加密后的密码
|
||||
*/
|
||||
export const rsaPsw = (password: string): string => {
|
||||
try {
|
||||
const encrypt = new JSEncrypt();
|
||||
const publicKey = RSA_PUBLIC_KEY;
|
||||
console.log('publicKey', publicKey);
|
||||
encrypt.setPublicKey(publicKey);
|
||||
const encrypted = encrypt.encrypt(Base64.encode(password));
|
||||
return encrypted || password;
|
||||
} catch (error) {
|
||||
console.error('RSA encryption failed:', error);
|
||||
return password;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取RSA公钥
|
||||
* @returns RSA公钥字符串
|
||||
*/
|
||||
export const getRSAPublicKey = (): string => {
|
||||
return RSA_PUBLIC_KEY;
|
||||
};
|
||||
|
||||
export const setRSAPublicKey = (key: string) => {
|
||||
RSA_PUBLIC_KEY = key;
|
||||
};
|
||||
@@ -120,26 +120,16 @@ request.interceptors.response.use(
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response;
|
||||
}
|
||||
|
||||
console.log('interceptors.response ------ response', response);
|
||||
const data: ResponseType = response.data;
|
||||
|
||||
// 处理业务错误码
|
||||
if (data?.code === 100) {
|
||||
snackbar.error(data?.message);
|
||||
} else if (data?.code === 401) {
|
||||
// notification.error({
|
||||
// message: data?.message,
|
||||
// description: data?.message,
|
||||
// duration: 3,
|
||||
// });
|
||||
notification.error(data?.message);
|
||||
redirectToLogin();
|
||||
} else if (data?.code !== 0) {
|
||||
// notification.error({
|
||||
// message: `${i18n.t('message.hint')} : ${data?.code}`,
|
||||
// description: data?.message,
|
||||
// duration: 3,
|
||||
// });
|
||||
notification.error(`${i18n.t('message.hint')} : ${data?.code}`, data?.message);
|
||||
}
|
||||
|
||||
@@ -148,19 +138,10 @@ request.interceptors.response.use(
|
||||
(error) => {
|
||||
// 处理网络错误
|
||||
if (error.message === FAILED_TO_FETCH || !error.response) {
|
||||
// notification.error({
|
||||
// description: i18n.t('message.networkAnomalyDescription'),
|
||||
// message: i18n.t('message.networkAnomaly'),
|
||||
// });
|
||||
notification.error(i18n.t('message.networkAnomaly'), i18n.t('message.networkAnomalyDescription'));
|
||||
} else if (error.response) {
|
||||
const { status, statusText } = error.response;
|
||||
const errorText = RetcodeMessage[status as ResultCode] || statusText;
|
||||
|
||||
// notification.error({
|
||||
// message: `${i18n.t('message.requestError')} ${status}`,
|
||||
// description: errorText,
|
||||
// });
|
||||
notification.error(`${i18n.t('message.requestError')} ${status}`, errorText);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user