feat(profile): Добавлена поддержка управления сессиями

This commit is contained in:
Андрей Дувакин 2025-07-03 13:15:56 +05:00
parent dc47e4b003
commit 59b77a665b
5 changed files with 144 additions and 24 deletions

View File

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

View File

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

View File

@ -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;
export const {useLoginMutation, useGetSessionsQuery, useLogoutSessionMutation, useLogoutAllSessionsMutation} = authApi;

View File

@ -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 = () => {
<Result
status="error"
title="Ошибка"
subTitle="Произошла ошибка при загрузке данных профиля"
subTitle="Произошла ошибка при загрузке данных профиля или сессий"
/>
);
}
@ -32,27 +38,31 @@ const ProfilePage = () => {
<LoadingIndicator/>
) : (
<>
<Typography.Title level={1}>
<Title level={1}>
<UserOutlined/> {userData.last_name} {userData.first_name}
</Typography.Title>
<Typography.Title level={1}>Профиль</Typography.Title>
<Card style={cardStyle}>
</Title>
<Title level={2}>Профиль</Title>
<Card style={cardStyle} title="Информация о пользователе">
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Text strong>Фамилия: </Typography.Text>
<Typography.Text>{userData.last_name || "-"}</Typography.Text>
<Text strong>Фамилия: </Text>
<Text>{userData.last_name || "-"}</Text>
</Col>
<Col span={24}>
<Typography.Text strong>Имя: </Typography.Text>
<Typography.Text>{userData.first_name || "-"}</Typography.Text>
<Text strong>Имя: </Text>
<Text>{userData.first_name || "-"}</Text>
</Col>
<Col span={24}>
<Typography.Text strong>Отчество: </Typography.Text>
<Typography.Text>{userData.patronymic || "-"}</Typography.Text>
<Text strong>Отчество: </Text>
<Text>{userData.patronymic || "-"}</Text>
</Col>
<Col span={24}>
<Typography.Text strong>Логин: </Typography.Text>
<Typography.Text>{userData.login || "-"}</Typography.Text>
<Text strong>Логин: </Text>
<Text>{userData.login || "-"}</Text>
</Col>
<Col span={24}>
<Text strong>Роль: </Text>
<Text>{userData.role?.title || "-"}</Text>
</Col>
</Row>
<Button
@ -65,6 +75,44 @@ const ProfilePage = () => {
</Button>
</Card>
<Card style={cardStyle} title="Активные сессии">
<List
dataSource={sessions}
renderItem={(session) => (
<List.Item
actions={[
<Button
key={session.id}
type="primary"
danger
onClick={() => handleLogoutSession(session.id)}
loading={isLoggingOutSession}
disabled={isLoggingOutSession}
>
Завершить
</Button>,
]}
>
<List.Item.Meta
title={`Устройство: ${session.device_info || "Неизвестно"}`}
description={`Создана: ${new Date(session.created_at).toLocaleString()} | Истекает: ${new Date(session.expires_at).toLocaleString()}`}
/>
</List.Item>
)}
/>
<Space style={{marginTop: 16}}>
<Button
type="primary"
danger
onClick={handleLogoutAllSessions}
loading={isLoggingOutAll}
disabled={isLoggingOutAll}
>
Завершить все сессии
</Button>
</Space>
</Card>
<UpdateUserModalForm/>
</>
)}

View File

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