diff --git a/api/app/application/sessions_repository.py b/api/app/application/sessions_repository.py index c3a34af..e1a1bb5 100644 --- a/api/app/application/sessions_repository.py +++ b/api/app/application/sessions_repository.py @@ -22,7 +22,7 @@ class SessionsRepository: result = await self.db.execute( select(Session).filter_by(id=session_id) ) - return result.scalars().all() + return result.scalars().first() async def get_by_token(self, token: str) -> Optional[Session]: result = await self.db.execute( diff --git a/api/app/infrastructure/auth_service.py b/api/app/infrastructure/auth_service.py index 9be0f83..2f46302 100644 --- a/api/app/infrastructure/auth_service.py +++ b/api/app/infrastructure/auth_service.py @@ -22,7 +22,7 @@ class AuthService: user = await self.users_repository.get_by_login(login) if user and user.check_password(password): access_token = self.create_access_token({"user_id": user.id}) - # Создаем сессию + session = Session( user_id=user.id, token=access_token, diff --git a/web-app/src/Api/authApi.js b/web-app/src/Api/authApi.js index 27ef36b..0b7b0c3 100644 --- a/web-app/src/Api/authApi.js +++ b/web-app/src/Api/authApi.js @@ -1,7 +1,6 @@ import {createApi} from "@reduxjs/toolkit/query/react"; import {baseQueryWithAuth} from "./baseQuery.js"; - export const authApi = createApi({ reducerPath: "authApi", baseQuery: baseQueryWithAuth, @@ -13,7 +12,25 @@ export const authApi = createApi({ body: credentials, }), }), + getSessions: builder.query({ + query: () => ({ + url: "/sessions/", + method: "GET", + }), + }), + logoutSession: builder.mutation({ + query: (sessionId) => ({ + url: `/sessions/${sessionId}/logout/`, + method: "POST", + }), + }), + logoutAllSessions: builder.mutation({ + query: () => ({ + url: "/sessions/logout_all/", + method: "POST", + }), + }), }), }); -export const {useLoginMutation} = authApi; \ No newline at end of file +export const {useLoginMutation, useGetSessionsQuery, useLogoutSessionMutation, useLogoutAllSessionsMutation} = authApi; \ No newline at end of file diff --git a/web-app/src/Components/Pages/ProfilePage/ProfilePage.jsx b/web-app/src/Components/Pages/ProfilePage/ProfilePage.jsx index e61f1f1..e4344f4 100644 --- a/web-app/src/Components/Pages/ProfilePage/ProfilePage.jsx +++ b/web-app/src/Components/Pages/ProfilePage/ProfilePage.jsx @@ -1,11 +1,12 @@ -import {Button, Card, Col, Row, Typography, Result} from "antd"; +import {Button, Card, Col, Row, Typography, Result, List, Space} from "antd"; import {EditOutlined, UserOutlined} from "@ant-design/icons"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import useProfilePage from "./useProfilePage.js"; import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx"; -const ProfilePage = () => { +const {Title, Text} = Typography; +const ProfilePage = () => { const { containerStyle, cardStyle, @@ -13,7 +14,12 @@ const ProfilePage = () => { isLoading, isError, userData, + sessions, handleEditUser, + handleLogoutSession, + handleLogoutAllSessions, + isLoggingOutSession, + isLoggingOutAll, } = useProfilePage(); if (isError) { @@ -21,7 +27,7 @@ const ProfilePage = () => { ); } @@ -32,27 +38,31 @@ const ProfilePage = () => { ) : ( <> - + <UserOutlined/> {userData.last_name} {userData.first_name} - </Typography.Title> - <Typography.Title level={1}>Профиль</Typography.Title> - <Card style={cardStyle}> + + Профиль + - Фамилия: - {userData.last_name || "-"} + Фамилия: + {userData.last_name || "-"} - Имя: - {userData.first_name || "-"} + Имя: + {userData.first_name || "-"} - Отчество: - {userData.patronymic || "-"} + Отчество: + {userData.patronymic || "-"} - Логин: - {userData.login || "-"} + Логин: + {userData.login || "-"} + + + Роль: + {userData.role?.title || "-"} + + ( + handleLogoutSession(session.id)} + loading={isLoggingOutSession} + disabled={isLoggingOutSession} + > + Завершить + , + ]} + > + + + )} + /> + + + + + )} diff --git a/web-app/src/Components/Pages/ProfilePage/useProfilePage.js b/web-app/src/Components/Pages/ProfilePage/useProfilePage.js index 630a955..9cac43a 100644 --- a/web-app/src/Components/Pages/ProfilePage/useProfilePage.js +++ b/web-app/src/Components/Pages/ProfilePage/useProfilePage.js @@ -1,40 +1,95 @@ import {Grid} from "antd"; -import {useDispatch} from "react-redux"; +import {useDispatch, useSelector} from "react-redux"; +import {useNavigate} from "react-router-dom"; import {setSelectedUser} from "../../../Redux/Slices/usersSlice.js"; import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js"; +import {useGetSessionsQuery, useLogoutSessionMutation, useLogoutAllSessionsMutation} from "../../../Api/authApi.js"; +import {logout, setError} from "../../../Redux/Slices/authSlice.js"; const {useBreakpoint} = Grid; const useProfilePage = () => { const dispatch = useDispatch(); + const navigate = useNavigate(); const screens = useBreakpoint(); + const {user} = useSelector((state) => state.auth); const { data: userData = {}, isLoading: isLoadingUserData, isError: isErrorUserData, } = useGetAuthenticatedUserDataQuery(undefined, { + pollingInterval: 10000, + }); + + const { + data: sessions = [], + isLoading: isLoadingSessions, + isError: isErrorSessions, + error: sessionsError, + } = useGetSessionsQuery(undefined, { + skip: !user, pollingInterval: 20000, }); + const [logoutSession, {isLoading: isLoggingOutSession}] = useLogoutSessionMutation(); + const [logoutAllSessions, {isLoading: isLoggingOutAll}] = useLogoutAllSessionsMutation(); + const containerStyle = {padding: screens.xs ? 16 : 24}; const cardStyle = {marginBottom: 24}; const buttonStyle = {width: screens.xs ? "100%" : "auto"}; - const handleEditUser = () => { - dispatch(setSelectedUser(userData)) + dispatch(setSelectedUser(userData)); }; + const handleLogoutSession = async (sessionId) => { + try { + await logoutSession(sessionId).unwrap(); + } catch (error) { + const errorMessage = error?.data?.detail || "Не удалось завершить сессию"; + dispatch(setError(errorMessage)); + if (error?.status === 401) { + dispatch(logout()); + navigate("/login"); + } + } + }; + + const handleLogoutAllSessions = async () => { + try { + await logoutAllSessions().unwrap(); + dispatch(logout()); + navigate("/login"); + } catch (error) { + const errorMessage = error?.data?.detail || "Не удалось завершить все сессии"; + dispatch(setError(errorMessage)); + if (error?.status === 401) { + dispatch(logout()); + navigate("/login"); + } + } + }; + + if (isErrorSessions && sessionsError?.status === 401) { + dispatch(logout()); + navigate("/login"); + } + return { userData, + sessions, containerStyle, cardStyle, buttonStyle, isMobile: screens.xs, - isLoading: isLoadingUserData, - isError: isErrorUserData, + isLoading: isLoadingUserData || isLoadingSessions, + isError: isErrorUserData || isErrorSessions, handleEditUser, + handleLogoutSession, + handleLogoutAllSessions, + isLoggingOutSession, + isLoggingOutAll, }; };