сделал управление пользователями

This commit is contained in:
Андрей Дувакин 2025-11-28 13:40:14 +05:00
parent 12c448aef9
commit 0a8e027b87
16 changed files with 395 additions and 12 deletions

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, List, Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@ -10,6 +10,11 @@ class StatusesRepository:
def __init__(self, db: AsyncSession) -> None:
self.db = db
async def get_all(self) -> Sequence[Status]:
query = select(Status)
results = await self.db.execute(query)
return results.scalars().all()
async def get_by_id(self, status_id: int) -> Optional[Status]:
query = (
select(Status)

View File

@ -0,0 +1,26 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.statuses import StatusRead
from app.domain.models import User
from app.infrastructure.dependencies import require_auth_user
from app.infrastructure.statuses_servise import StatusesService
statuses_router = APIRouter()
@statuses_router.get(
'/',
response_model=List[StatusRead],
summary='Return all statuses',
description='Return all statuses',
)
async def get_statuses(
db: AsyncSession = Depends(get_db),
user: User = Depends(require_auth_user),
):
statuses_service = StatusesService(db)
return await statuses_service.get_all()

View File

@ -72,6 +72,7 @@ async def change_password(
users_service = UsersService(db)
return await users_service.change_password(user_id, password_data, user)
@users_router.post(
'/create/',
response_model=Optional[UserRead],

View File

@ -34,8 +34,11 @@ class UserUpdate(BaseModel):
first_name: str = Field(max_length=250)
last_name: str = Field(max_length=250)
patronymic: Optional[str] = Field(default=None, max_length=250)
login: str = Field(max_length=250)
email: Optional[EmailStr] = None
birthdate: date
role_id: Optional[int] = Field(default=None)
status_id: Optional[int] = Field(default=None)
class PasswordChangeRequest(BaseModel):

View File

@ -0,0 +1,21 @@
from typing import List
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.statuses_repository import StatusesRepository
from app.domain.entities.statuses import StatusRead
class StatusesService:
def __init__(self, db: AsyncSession):
self.statuses_repository = StatusesRepository(db)
async def get_all(self) -> List[StatusRead]:
statuses = await self.statuses_repository.get_all()
response = []
for status in statuses:
response.append(
StatusRead.model_validate(status)
)
return response

View File

@ -42,7 +42,7 @@ class UsersService:
detail='Пользователь не найден',
)
if current_user.id != user_model.id and not current_user.role.title != self.settings.root_role_name:
if current_user.id != user_model.id and current_user.role.title != self.settings.root_role_name:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
@ -54,6 +54,10 @@ class UsersService:
user_model.email = user.email
user_model.birthdate = user.birthdate
if current_user.role.title == self.settings.root_role_name and user.role_id is not None:
user_model.role_id = user.role_id
user_model.status_id = user.status_id
user_model = await self.users_repository.update(user_model)
return UserRead.model_validate(user_model)
@ -68,7 +72,7 @@ class UsersService:
detail='Пользователь не найден',
)
if current_user.id != user_model.id and not current_user.role.title != self.settings.root_role_name:
if current_user.id != user_model.id and current_user.role.title != self.settings.root_role_name:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',

View File

@ -4,6 +4,7 @@ from starlette.middleware.cors import CORSMiddleware
from app.controllers.auth_router import auth_router
from app.controllers.register_router import register_router
from app.controllers.roles_router import roles_router
from app.controllers.statuses_router import statuses_router
from app.controllers.users_router import users_router
from app.settings import Settings
@ -23,6 +24,7 @@ def start_app():
api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth'])
api_app.include_router(register_router, prefix=f'{settings.prefix}/register', tags=['register'])
api_app.include_router(roles_router, prefix=f'{settings.prefix}/roles', tags=['roles'])
api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])
api_app.include_router(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
return api_app

View File

@ -0,0 +1,20 @@
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
import CONFIG from "../Core/сonfig.js";
import {baseQueryWithAuth} from "./baseQuery.js";
export const statusesApi = createApi({
reducerPath: 'statusesApi',
baseQuery: baseQueryWithAuth,
tagTypes: ['Statuses'],
endpoints: (builder) => ({
getStatuses: builder.query({
query: () => '/statuses/',
providesTags: ['Statuses'],
}),
}),
});
export const {
useGetStatusesQuery,
} = statusesApi;

View File

@ -13,6 +13,7 @@ export const usersApi = createApi({
}),
getAuthenticatedUserData: builder.query({
query: () => "/users/me/",
providesTags: ["user"],
}),
updateUser: builder.mutation({
query: ({userId, ...data}) => ({

View File

@ -3,6 +3,7 @@ import {ControlOutlined, PlusOutlined} from "@ant-design/icons";
import useAdminPage from "./useAdminPage.js";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import CreateUserModalForm from "./CreateUserModalForm/CreateUserModalForm.jsx";
import UpdateUserModalForm from "./UpdateUserModalForm/UpdateUserModalForm.jsx";
const {Title} = Typography;
@ -15,6 +16,7 @@ const AdminPage = () => {
isLoading,
isError,
openCreateModal,
currentUser,
} = useAdminPage();
const columns = [
@ -47,6 +49,13 @@ const AdminPage = () => {
filters: rolesData.map(role => ({text: role.title, value: role.title})),
onFilter: (value, record) => record.role.title === value,
},
{
title: "Статус",
dataIndex: ["status", "title"],
key: "status",
filters: rolesData.map(status => ({text: status.title, value: status.title})),
onFilter: (value, record) => record.status.title === value,
},
{
title: "Статус",
dataIndex: "status",
@ -56,9 +65,9 @@ const AdminPage = () => {
title: "Действия",
key: "actions",
render: (_, record) => (
<Button type="link" onClick={() => handleSelectUserToEdit(record)}>
(currentUser.id !== record.id ? (<Button type="link" onClick={() => handleSelectUserToEdit(record)}>
Редактировать
</Button>
</Button>) : null)
),
},
];
@ -97,6 +106,7 @@ const AdminPage = () => {
</Col>
<CreateUserModalForm/>
<UpdateUserModalForm/>
</Row>
)
};

View File

@ -36,8 +36,6 @@ const useCreateUserModalForm = () => {
: null,
};
console.log(payload);
try {
await registerUser(payload).unwrap();
notification.success({

View File

@ -0,0 +1,147 @@
import {Button, DatePicker, Form, Input, Modal, Result, Select, Tooltip, Typography} from "antd";
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
import {CalendarOutlined, InfoCircleOutlined} from "@ant-design/icons";
import dayjs from "dayjs";
import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
const UpdateUserModalForm = () => {
const {
modalVisible,
handleCancel,
handleFinish,
userForm,
passwordForm,
roles,
isLoading,
isError,
isLoadingUpdate,
isErrorUpdate,
handlePasswordFinish,
statusesData,
} = useUpdateUserModalForm();
if (isLoading) {
return <LoadingIndicator/>
}
if (isError) {
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>
}
return (
<Modal
title="Изменить пользователя"
open={modalVisible}
onCancel={handleCancel}
footer={null}
>
<Form
form={userForm}
onFinish={handleFinish}
layout="vertical"
>
<Form.Item label="Фамилия" name="last_name" rules={[{required: true, message: "Введите фамилию"}]}>
<Input/>
</Form.Item>
<Form.Item label="Имя" name="first_name" rules={[{required: true, message: "Введите имя"}]}>
<Input/>
</Form.Item>
<Form.Item label="Отчество" name="patronymic">
<Input/>
</Form.Item>
<Form.Item label="Логин" name="login" rules={[{required: true, message: "Введите логин"}]}>
<Input disabled/>
</Form.Item>
<Form.Item
name="email"
label="Email"
rules={[{required: true, message: "Введите email", type: "email"}]}>
<Input/>
</Form.Item>
<Form.Item
name="birthdate"
label="Дата рождения"
rules={[{required: true, message: "Введите дату рождения"}]}
>
<DatePicker
suffixIcon={<CalendarOutlined/>}
format="DD.MM.YYYY"
style={{width: "100%"}}
size="large"
maxDate={dayjs()}
/>
</Form.Item>
<Form.Item
name="role_id"
label="Роль"
rules={[{required: true, message: "Выберите роль"}]}
>
<Select>
{roles.map((role) => (
<Select.Option key={role.id} value={role.id}>
{role.title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="status_id"
label="Статус"
rules={[{required: true, message: "Выберите статус"}]}
>
<Select>
{statusesData.map((status) => (
<Select.Option key={status.id} value={status.id}>
{status.title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}>
Сохранить изменения
</Button>
</Form.Item>
</Form>
<Form form={passwordForm} onFinish={handlePasswordFinish}>
<Typography.Title level={4}>Изменение пароля</Typography.Title>
<Form.Item
name="password"
label="Пароль"
rules={[{required: true, message: "Введите пароль"}]}
>
<Input.Password/>
</Form.Item>
<Form.Item
name="repeat_password"
label="Подтверждение пароля"
dependencies={["password"]}
rules={[
{required: true, message: "Повторите пароль"},
({getFieldValue}) => ({
validator(_, value) {
if (!value || getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(new Error("Пароли не совпадают"));
},
}),
]}
>
<Input.Password/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}>
Изменить пароль
</Button>
<Button onClick={handleCancel} style={{marginLeft: 8}}>
Отмена
</Button>
</Form.Item>
</Form>
</Modal>
)
};
export default UpdateUserModalForm;

View File

@ -0,0 +1,137 @@
import {useDispatch, useSelector} from "react-redux";
import {Form, notification} from "antd";
import {setSelectedUserToUpdate} from "../../../../Redux/Slices/usersSlice.js";
import {useGetAllRolesQuery} from "../../../../Api/rolesApi.js";
import {useEffect} from "react";
import dayjs from "dayjs";
import {useUpdateUserMutation, useUpdateUserPasswordMutation} from "../../../../Api/usersApi.js";
import {useGetStatusesQuery} from "../../../../Api/statusesApi.js";
const useUpdateUserModalForm = () => {
const dispatch = useDispatch();
const [userForm] = Form.useForm();
const [passwordForm] = Form.useForm();
const {
selectedUserToUpdate
} = useSelector(state => state.users);
const modalVisible = selectedUserToUpdate !== null;
const handleCancel = () => {
dispatch(setSelectedUserToUpdate(null));
};
const [
updateUser,
{
isLoading: isLoadingUpdate,
isError: isErrorUpdate,
}
] = useUpdateUserMutation();
const [
changeUserPassword,
{
isLoading: isLoadingChangePassword,
isError: isErrorChangePassword,
}
] = useUpdateUserPasswordMutation();
const {
data: statusesData = [],
isLoading: statusesIsLoading,
isError: statusesIsError,
} = useGetStatusesQuery(undefined);
useEffect(() => {
if (selectedUserToUpdate) {
const { role, ...userWithoutRole } = selectedUserToUpdate;
userForm.setFieldsValue({
...userWithoutRole,
role_id: role?.id,
birthdate: selectedUserToUpdate.birthdate
? dayjs(selectedUserToUpdate.birthdate)
: null,
});
}
}, [selectedUserToUpdate, userForm]);
const handlePasswordFinish = async () => {
const values = passwordForm.getFieldsValue();
const payload = {
...values,
};
try {
await changeUserPassword({userId: selectedUserToUpdate.id, ...payload}).unwrap();
notification.success({
title: "Пароль изменен",
description: "Пароль успешно изменен",
placement: "topRight",
});
passwordForm.resetFields();
handleCancel();
} catch (error) {
notification.error({
title: "Ошибка изменения пароля",
description: error?.data?.detail || "Не удалось изменить пароль",
placement: "topRight",
});
}
};
const handleFinish = async () => {
const values = userForm.getFieldsValue();
const payload = {
...values,
birthdate: values.birthdate
? values.birthdate.format("YYYY-MM-DD")
: null,
};
try {
await updateUser({userId: selectedUserToUpdate.id, ...payload}).unwrap();
notification.success({
title: "Пользователь изменен",
description: "Пользователь успешно изменен",
placement: "topRight",
});
userForm.resetFields();
handleCancel();
} catch (error) {
notification.error({
title: "Ошибка изменения пользователя",
description: error?.data?.detail || "Не удалось изменить пользователя",
placement: "topRight",
});
}
};
const {
data: roles = [],
isLoading: isLoadingRoles,
isError: isErrorRoles,
} = useGetAllRolesQuery(undefined, {
pollingInterval: 60000,
});
return {
modalVisible,
handleCancel,
handleFinish,
userForm,
passwordForm,
roles,
isLoading: isLoadingRoles | statusesIsLoading,
isError: isErrorRoles | statusesIsError,
isLoadingUpdate,
isErrorUpdate,
handlePasswordFinish,
statusesData,
};
};
export default useUpdateUserModalForm;

View File

@ -1,4 +1,4 @@
import {useGetAllUsersQuery} from "../../../Api/usersApi.js";
import {useGetAllUsersQuery, useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
import {useGetAllRolesQuery} from "../../../Api/rolesApi.js";
import {useMemo, useState} from "react";
import {useDispatch} from "react-redux";
@ -7,6 +7,11 @@ import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../Redux/Sl
const useAdminPage = () => {
const dispatch = useDispatch();
const {
data: currentUser = {},
isLoading: currentUserIsLoading,
isError: currentUserIsError,
} = useGetAuthenticatedUserDataQuery(undefined);
const [
searchString,
@ -56,9 +61,10 @@ const useAdminPage = () => {
rolesData,
filteredUsers,
handleSearch,
isLoading: usersIsLoading | rolesIsLoading,
isError: usersIsError | rolesIsError,
isLoading: usersIsLoading | rolesIsLoading | currentUserIsLoading,
isError: usersIsError | rolesIsError | currentUserIsError,
openCreateModal,
currentUser,
};
};

View File

@ -148,10 +148,8 @@ const ProfilePage = () => {
</Form>
</Card>
{/* Карточка смены пароля */}
<Card
title={<Title level={4}><LockOutlined/> Смена пароля</Title>}
bordered={false}
style={{borderRadius: 12, boxShadow: "0 4px 12px rgba(0,0,0,0.05)"}}
>
<Form form={passwordForm} layout="vertical" onFinish={onFinishPasswordForm}>

View File

@ -4,6 +4,7 @@ import usersReducer from "./Slices/usersSlice.js";
import {authApi} from "../Api/authApi.js";
import {usersApi} from "../Api/usersApi.js";
import {rolesApi} from "../Api/rolesApi.js";
import {statusesApi} from "../Api/statusesApi.js";
export const store = configureStore({
reducer: {
@ -14,12 +15,15 @@ export const store = configureStore({
[usersApi.reducerPath]: usersApi.reducer,
[rolesApi.reducerPath]: rolesApi.reducer,
[statusesApi.reducerPath]: statusesApi.reducer
},
middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat(
authApi.middleware,
usersApi.middleware,
rolesApi.middleware,
statusesApi.middleware,
)
),
});