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:
2025-10-10 18:25:20 +08:00
parent a1282de74f
commit d6ff547f20
10 changed files with 697 additions and 119 deletions

4
.env
View File

@@ -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-----"

View File

@@ -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
View File

@@ -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: {}

View File

@@ -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>

View 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
View 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,
};
};

View File

@@ -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>

View File

@@ -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
View 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;
};

View File

@@ -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);
}