refactor: Авторизация через Redux Toolkit
Удален AuthContext, логика авторизации перенесена в Redux. Добавлены authSlice и authApi для управления состоянием авторизации.
This commit is contained in:
parent
0c326d815a
commit
4d903ee8c5
@ -1,21 +1,41 @@
|
||||
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { logout } from "../Redux/Slices/authSlice.js";
|
||||
import CONFIG from "../Core/сonfig.js";
|
||||
|
||||
const baseQuery = fetchBaseQuery({
|
||||
baseUrl: CONFIG.BASE_URL,
|
||||
prepareHeaders: (headers) => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
headers.set("Content-Type", "application/json");
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
||||
const baseQueryWithAuth = async (args, api, extraOptions) => {
|
||||
const result = await baseQuery(args, api, extraOptions);
|
||||
if (result.error && result.error.status === 401) {
|
||||
localStorage.removeItem("access_token");
|
||||
api.dispatch(logout());
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const authApi = createApi({
|
||||
reducerPath: 'authApi',
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: CONFIG.BASE_URL,
|
||||
}),
|
||||
reducerPath: "authApi",
|
||||
baseQuery: baseQueryWithAuth,
|
||||
endpoints: (builder) => ({
|
||||
login: builder.mutation({
|
||||
query: (credentials) => ({
|
||||
url: '/auth/login/',
|
||||
method: 'POST',
|
||||
body: credentials
|
||||
})
|
||||
url: "/login/",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
export const {useLoginMutation} = authApi;
|
||||
export const { useLoginMutation } = authApi;
|
||||
@ -1,6 +1,5 @@
|
||||
import {BrowserRouter as Router} from "react-router-dom";
|
||||
import AppRouter from "./AppRouter.jsx";
|
||||
import {AuthProvider} from "../Hooks/AuthContext.jsx";
|
||||
import "/src/Styles/app.css";
|
||||
import {Provider} from "react-redux";
|
||||
import store from "../Redux/store.js";
|
||||
@ -13,11 +12,9 @@ dayjs.locale('ru');
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<AuthProvider>
|
||||
<ConfigProvider locale={locale}>
|
||||
<AppRouter/>
|
||||
</ConfigProvider>
|
||||
</AuthProvider>
|
||||
<ConfigProvider locale={locale}>
|
||||
<AppRouter/>
|
||||
</ConfigProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
22
web-app/src/App/ErrorBoundary.jsx
Normal file
22
web-app/src/App/ErrorBoundary.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Component } from "react";
|
||||
|
||||
class ErrorBoundary extends Component {
|
||||
state = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
console.error("ErrorBoundary caught:", error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <div>Произошла ошибка</div>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@ -1,15 +1,14 @@
|
||||
import {Navigate, Outlet} from "react-router-dom";
|
||||
import {useAuth} from "../Hooks/AuthContext.jsx";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const PrivateRoute = () => {
|
||||
const {user} = useAuth();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login"/>;
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
return <Outlet/>;
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
|
||||
export default PrivateRoute;
|
||||
@ -11,7 +11,7 @@ import {
|
||||
LogoutOutlined,
|
||||
MessageOutlined
|
||||
} from "@ant-design/icons";
|
||||
import {useAuth} from "../../Hooks/AuthContext.jsx";
|
||||
import useAuthUtils from "../../Hooks/useAuthUtils.js";
|
||||
|
||||
const {Content, Footer, Sider} = Layout;
|
||||
|
||||
@ -24,7 +24,7 @@ const MainLayout = () => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const {logout} = useAuth();
|
||||
const {logoutAndRedirect} = useAuthUtils();
|
||||
|
||||
const menuItems = [
|
||||
getItem("Главная", "/", <HomeOutlined/>),
|
||||
@ -42,7 +42,7 @@ const MainLayout = () => {
|
||||
|
||||
const handleMenuClick = ({key}) => {
|
||||
if (key === "logout") {
|
||||
logout();
|
||||
logoutAndRedirect();
|
||||
return;
|
||||
}
|
||||
navigate(key);
|
||||
|
||||
@ -1,73 +1,48 @@
|
||||
import {Form, Input, Button, Row, Col, Typography} from 'antd';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useAuth} from "../../../Hooks/AuthContext.jsx";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {Form, Input, Button, Row, Col, Typography} from "antd";
|
||||
import {useSelector} from "react-redux";
|
||||
import useLoginPage from "./useLoginPage.js";
|
||||
import useLoginPageUI from "./useLoginPageUI.js";
|
||||
|
||||
const {Title} = Typography;
|
||||
|
||||
const LoginPage = () => {
|
||||
const {user, login} = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate("/");
|
||||
}
|
||||
document.title = "Авторизация";
|
||||
}, [user, navigate]);
|
||||
|
||||
const onFinish = async (values) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await login(values);
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
setError(`Ошибка при входе: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const {error} = useSelector((state) => state.auth);
|
||||
const {onFinish, isLoading} = useLoginPage();
|
||||
const {containerStyle, formContainerStyle, titleStyle, errorStyle, labels} = useLoginPageUI();
|
||||
|
||||
return (
|
||||
<Row justify="center" align="middle" style={{minHeight: '100vh'}}>
|
||||
<Row justify="center" align="middle" style={containerStyle}>
|
||||
<Col xs={24} sm={18} md={12} lg={8} xl={6}>
|
||||
<div style={{padding: 20, border: '1px solid #ddd', borderRadius: 8}}>
|
||||
<Title level={2} style={{textAlign: 'center'}}>Авторизация</Title>
|
||||
<div style={formContainerStyle}>
|
||||
<Title level={2} style={titleStyle}>
|
||||
{labels.title}
|
||||
</Title>
|
||||
|
||||
{error && <div style={{color: 'red', marginBottom: 15}}>{error}</div>}
|
||||
{error && (
|
||||
<div style={errorStyle}>
|
||||
{labels.errorPrefix}
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form
|
||||
name="login"
|
||||
initialValues={{remember: true}}
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<Form name="login" initialValues={{remember: true}} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="login"
|
||||
rules={[{required: true, message: 'Пожалуйста, введите логин'}]}
|
||||
rules={[{required: true, message: labels.loginRequired}]}
|
||||
>
|
||||
<Input placeholder="Логин"/>
|
||||
<Input placeholder={labels.loginPlaceholder}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: 'Пожалуйста, введите пароль'}]}
|
||||
rules={[{required: true, message: labels.passwordRequired}]}
|
||||
>
|
||||
<Input.Password placeholder="Пароль"/>
|
||||
<Input.Password placeholder={labels.passwordPlaceholder}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
loading={loading}
|
||||
>
|
||||
Войти
|
||||
<Button type="primary" htmlType="submit" block loading={isLoading}>
|
||||
{labels.submitButton}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@ -77,4 +52,4 @@ const LoginPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
export default LoginPage;
|
||||
36
web-app/src/Components/Pages/LoginPage/useLoginPage.js
Normal file
36
web-app/src/Components/Pages/LoginPage/useLoginPage.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { notification } from "antd";
|
||||
import {setError, setUser} from "../../../Redux/Slices/authSlice.js";
|
||||
import {useLoginMutation} from "../../../Api/authApi.js";
|
||||
|
||||
const useLoginPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [loginUser, { isLoading }] = useLoginMutation();
|
||||
|
||||
const onFinish = async (loginData) => {
|
||||
try {
|
||||
const response = await loginUser(loginData).unwrap();
|
||||
const token = response.access_token || response.token;
|
||||
if (!token) {
|
||||
throw new Error("Токен не получен от сервера");
|
||||
}
|
||||
localStorage.setItem("access_token", token);
|
||||
dispatch(setUser({ token }));
|
||||
} catch (error) {
|
||||
const errorMessage = error?.data?.message || "Не удалось войти";
|
||||
dispatch(setError(errorMessage));
|
||||
notification.error({
|
||||
message: "Ошибка при входе",
|
||||
description: errorMessage,
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onFinish,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLoginPage;
|
||||
58
web-app/src/Components/Pages/LoginPage/useLoginPageUI.js
Normal file
58
web-app/src/Components/Pages/LoginPage/useLoginPageUI.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Grid } from "antd";
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const useLoginPageUI = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const containerStyle = {
|
||||
minHeight: "100vh",
|
||||
};
|
||||
|
||||
const formContainerStyle = {
|
||||
padding: screens.xs ? 10 : 20,
|
||||
border: "1px solid #ddd",
|
||||
borderRadius: 8,
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
textAlign: "center",
|
||||
};
|
||||
|
||||
const errorStyle = {
|
||||
color: "red",
|
||||
marginBottom: 15,
|
||||
};
|
||||
|
||||
const labels = {
|
||||
title: "Авторизация",
|
||||
loginPlaceholder: "Логин",
|
||||
passwordPlaceholder: "Пароль",
|
||||
submitButton: "Войти",
|
||||
loginRequired: "Пожалуйста, введите логин",
|
||||
passwordRequired: "Пожалуйста, введите пароль",
|
||||
errorPrefix: "Ошибка при входе: ",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate("/");
|
||||
}
|
||||
document.title = labels.title;
|
||||
}, [user, navigate]);
|
||||
|
||||
return {
|
||||
containerStyle,
|
||||
formContainerStyle,
|
||||
titleStyle,
|
||||
errorStyle,
|
||||
labels,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLoginPageUI;
|
||||
@ -1,67 +0,0 @@
|
||||
import {createContext, useState, useContext, useEffect} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import loginUser from "../old_api/auth/loginRequest.js";
|
||||
import {Spin} from "antd";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import createApi from "../Core/axiosConfig.js";
|
||||
|
||||
const AuthContext = createContext(undefined);
|
||||
|
||||
const AuthProvider = ({children}) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
setUser({token});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
|
||||
const login = async (loginData) => {
|
||||
try {
|
||||
const token = await loginUser(loginData, api);
|
||||
localStorage.setItem("access_token", token);
|
||||
setUser({token});
|
||||
} catch (error) {
|
||||
console.error("Login failed", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const logoutAndRedirect = () => {
|
||||
localStorage.removeItem("access_token");
|
||||
setUser(null);
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem("access_token");
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Spin/>;
|
||||
}
|
||||
|
||||
const api = createApi(logoutAndRedirect);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{user, login, logout, logoutAndRedirect, api}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
AuthProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const useAuth = () => {
|
||||
return useContext(AuthContext);
|
||||
};
|
||||
|
||||
export {useAuth, AuthProvider};
|
||||
18
web-app/src/Hooks/useAuthUtils.js
Normal file
18
web-app/src/Hooks/useAuthUtils.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { logout } from "../Redux/Slices/authSlice.js";
|
||||
|
||||
const useAuthUtils = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const logoutAndRedirect = () => {
|
||||
localStorage.removeItem("access_token");
|
||||
dispatch(logout());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return { logoutAndRedirect };
|
||||
};
|
||||
|
||||
export default useAuthUtils;
|
||||
49
web-app/src/Redux/Slices/authSlice.js
Normal file
49
web-app/src/Redux/Slices/authSlice.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
|
||||
export const checkAuth = createAsyncThunk("auth/checkAuth", async () => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
return { token };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
user: null,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
setUser(state, action) {
|
||||
state.user = action.payload;
|
||||
state.isLoading = false;
|
||||
state.error = null;
|
||||
},
|
||||
setError(state, action) {
|
||||
state.error = action.payload;
|
||||
state.isLoading = false;
|
||||
},
|
||||
logout(state) {
|
||||
state.user = null;
|
||||
state.error = null;
|
||||
state.isLoading = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(checkAuth.fulfilled, (state, action) => {
|
||||
state.user = action.payload;
|
||||
state.isLoading = false;
|
||||
})
|
||||
.addCase(checkAuth.rejected, (state) => {
|
||||
state.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setUser, setError, logout } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
@ -15,6 +15,8 @@ import {scheduledAppointmentsApi} from "../Api/scheduledAppointmentsApi.js";
|
||||
import {appointmentTypesApi} from "../Api/appointmentTypesApi.js";
|
||||
import {usersApi} from "../Api/usersApi.js";
|
||||
import usersReducer from "./Slices/usersSlice.js";
|
||||
import {authApi} from "../Api/authApi.js";
|
||||
import authReducer from "./Slices/authSlice.js";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@ -42,7 +44,10 @@ export const store = configureStore({
|
||||
[appointmentTypesApi.reducerPath]: appointmentTypesApi.reducer,
|
||||
|
||||
[usersApi.reducerPath]: usersApi.reducer,
|
||||
usersUI: usersReducer
|
||||
usersUI: usersReducer,
|
||||
|
||||
auth: authReducer,
|
||||
[authApi.reducerPath]: authApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => (
|
||||
getDefaultMiddleware().concat(
|
||||
@ -56,6 +61,7 @@ export const store = configureStore({
|
||||
scheduledAppointmentsApi.middleware,
|
||||
appointmentTypesApi.middleware,
|
||||
usersApi.middleware,
|
||||
authApi.middleware,
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import CONFIG from "../../Core/сonfig.js";
|
||||
|
||||
const loginUser = async (loginData, api) => {
|
||||
const response = await api.post(`${CONFIG.BASE_URL}/login/`, loginData, {
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return response.data.access_token;
|
||||
};
|
||||
|
||||
export default loginUser;
|
||||
Loading…
x
Reference in New Issue
Block a user