feat(profile): Добавлена поддержка управления сессиями
This commit is contained in:
parent
dc47e4b003
commit
59b77a665b
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user