feat(profile): Добавлена поддержка управления сессиями
This commit is contained in:
parent
dc47e4b003
commit
59b77a665b
@ -22,7 +22,7 @@ class SessionsRepository:
|
|||||||
result = await self.db.execute(
|
result = await self.db.execute(
|
||||||
select(Session).filter_by(id=session_id)
|
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]:
|
async def get_by_token(self, token: str) -> Optional[Session]:
|
||||||
result = await self.db.execute(
|
result = await self.db.execute(
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class AuthService:
|
|||||||
user = await self.users_repository.get_by_login(login)
|
user = await self.users_repository.get_by_login(login)
|
||||||
if user and user.check_password(password):
|
if user and user.check_password(password):
|
||||||
access_token = self.create_access_token({"user_id": user.id})
|
access_token = self.create_access_token({"user_id": user.id})
|
||||||
# Создаем сессию
|
|
||||||
session = Session(
|
session = Session(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
token=access_token,
|
token=access_token,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import {createApi} from "@reduxjs/toolkit/query/react";
|
import {createApi} from "@reduxjs/toolkit/query/react";
|
||||||
import {baseQueryWithAuth} from "./baseQuery.js";
|
import {baseQueryWithAuth} from "./baseQuery.js";
|
||||||
|
|
||||||
|
|
||||||
export const authApi = createApi({
|
export const authApi = createApi({
|
||||||
reducerPath: "authApi",
|
reducerPath: "authApi",
|
||||||
baseQuery: baseQueryWithAuth,
|
baseQuery: baseQueryWithAuth,
|
||||||
@ -13,7 +12,25 @@ export const authApi = createApi({
|
|||||||
body: credentials,
|
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 {EditOutlined, UserOutlined} from "@ant-design/icons";
|
||||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
import useProfilePage from "./useProfilePage.js";
|
import useProfilePage from "./useProfilePage.js";
|
||||||
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
import UpdateUserModalForm from "../../Dummies/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const {Title, Text} = Typography;
|
||||||
|
|
||||||
|
const ProfilePage = () => {
|
||||||
const {
|
const {
|
||||||
containerStyle,
|
containerStyle,
|
||||||
cardStyle,
|
cardStyle,
|
||||||
@ -13,7 +14,12 @@ const ProfilePage = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
userData,
|
userData,
|
||||||
|
sessions,
|
||||||
handleEditUser,
|
handleEditUser,
|
||||||
|
handleLogoutSession,
|
||||||
|
handleLogoutAllSessions,
|
||||||
|
isLoggingOutSession,
|
||||||
|
isLoggingOutAll,
|
||||||
} = useProfilePage();
|
} = useProfilePage();
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@ -21,7 +27,7 @@ const ProfilePage = () => {
|
|||||||
<Result
|
<Result
|
||||||
status="error"
|
status="error"
|
||||||
title="Ошибка"
|
title="Ошибка"
|
||||||
subTitle="Произошла ошибка при загрузке данных профиля"
|
subTitle="Произошла ошибка при загрузке данных профиля или сессий"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -32,27 +38,31 @@ const ProfilePage = () => {
|
|||||||
<LoadingIndicator/>
|
<LoadingIndicator/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Typography.Title level={1}>
|
<Title level={1}>
|
||||||
<UserOutlined/> {userData.last_name} {userData.first_name}
|
<UserOutlined/> {userData.last_name} {userData.first_name}
|
||||||
</Typography.Title>
|
</Title>
|
||||||
<Typography.Title level={1}>Профиль</Typography.Title>
|
<Title level={2}>Профиль</Title>
|
||||||
<Card style={cardStyle}>
|
<Card style={cardStyle} title="Информация о пользователе">
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Typography.Text strong>Фамилия: </Typography.Text>
|
<Text strong>Фамилия: </Text>
|
||||||
<Typography.Text>{userData.last_name || "-"}</Typography.Text>
|
<Text>{userData.last_name || "-"}</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Typography.Text strong>Имя: </Typography.Text>
|
<Text strong>Имя: </Text>
|
||||||
<Typography.Text>{userData.first_name || "-"}</Typography.Text>
|
<Text>{userData.first_name || "-"}</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Typography.Text strong>Отчество: </Typography.Text>
|
<Text strong>Отчество: </Text>
|
||||||
<Typography.Text>{userData.patronymic || "-"}</Typography.Text>
|
<Text>{userData.patronymic || "-"}</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Typography.Text strong>Логин: </Typography.Text>
|
<Text strong>Логин: </Text>
|
||||||
<Typography.Text>{userData.login || "-"}</Typography.Text>
|
<Text>{userData.login || "-"}</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Text strong>Роль: </Text>
|
||||||
|
<Text>{userData.role?.title || "-"}</Text>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Button
|
<Button
|
||||||
@ -65,6 +75,44 @@ const ProfilePage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</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/>
|
<UpdateUserModalForm/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,40 +1,95 @@
|
|||||||
import {Grid} from "antd";
|
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 {setSelectedUser} from "../../../Redux/Slices/usersSlice.js";
|
||||||
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.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 {useBreakpoint} = Grid;
|
||||||
|
|
||||||
const useProfilePage = () => {
|
const useProfilePage = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const screens = useBreakpoint();
|
const screens = useBreakpoint();
|
||||||
|
const {user} = useSelector((state) => state.auth);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: userData = {},
|
data: userData = {},
|
||||||
isLoading: isLoadingUserData,
|
isLoading: isLoadingUserData,
|
||||||
isError: isErrorUserData,
|
isError: isErrorUserData,
|
||||||
} = useGetAuthenticatedUserDataQuery(undefined, {
|
} = useGetAuthenticatedUserDataQuery(undefined, {
|
||||||
|
pollingInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: sessions = [],
|
||||||
|
isLoading: isLoadingSessions,
|
||||||
|
isError: isErrorSessions,
|
||||||
|
error: sessionsError,
|
||||||
|
} = useGetSessionsQuery(undefined, {
|
||||||
|
skip: !user,
|
||||||
pollingInterval: 20000,
|
pollingInterval: 20000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [logoutSession, {isLoading: isLoggingOutSession}] = useLogoutSessionMutation();
|
||||||
|
const [logoutAllSessions, {isLoading: isLoggingOutAll}] = useLogoutAllSessionsMutation();
|
||||||
|
|
||||||
const containerStyle = {padding: screens.xs ? 16 : 24};
|
const containerStyle = {padding: screens.xs ? 16 : 24};
|
||||||
const cardStyle = {marginBottom: 24};
|
const cardStyle = {marginBottom: 24};
|
||||||
const buttonStyle = {width: screens.xs ? "100%" : "auto"};
|
const buttonStyle = {width: screens.xs ? "100%" : "auto"};
|
||||||
|
|
||||||
|
|
||||||
const handleEditUser = () => {
|
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 {
|
return {
|
||||||
userData,
|
userData,
|
||||||
|
sessions,
|
||||||
containerStyle,
|
containerStyle,
|
||||||
cardStyle,
|
cardStyle,
|
||||||
buttonStyle,
|
buttonStyle,
|
||||||
isMobile: screens.xs,
|
isMobile: screens.xs,
|
||||||
isLoading: isLoadingUserData,
|
isLoading: isLoadingUserData || isLoadingSessions,
|
||||||
isError: isErrorUserData,
|
isError: isErrorUserData || isErrorSessions,
|
||||||
handleEditUser,
|
handleEditUser,
|
||||||
|
handleLogoutSession,
|
||||||
|
handleLogoutAllSessions,
|
||||||
|
isLoggingOutSession,
|
||||||
|
isLoggingOutAll,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user