сделал управление пользователями
This commit is contained in:
parent
12c448aef9
commit
0a8e027b87
@ -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)
|
||||
|
||||
26
api/app/controllers/statuses_router.py
Normal file
26
api/app/controllers/statuses_router.py
Normal 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()
|
||||
@ -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],
|
||||
|
||||
@ -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):
|
||||
|
||||
21
api/app/infrastructure/statuses_servise.py
Normal file
21
api/app/infrastructure/statuses_servise.py
Normal 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
|
||||
@ -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='Доступ запрещен',
|
||||
|
||||
@ -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
|
||||
|
||||
20
web/src/Api/statusesApi.js
Normal file
20
web/src/Api/statusesApi.js
Normal 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;
|
||||
@ -13,6 +13,7 @@ export const usersApi = createApi({
|
||||
}),
|
||||
getAuthenticatedUserData: builder.query({
|
||||
query: () => "/users/me/",
|
||||
providesTags: ["user"],
|
||||
}),
|
||||
updateUser: builder.mutation({
|
||||
query: ({userId, ...data}) => ({
|
||||
|
||||
@ -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>
|
||||
)
|
||||
};
|
||||
|
||||
@ -36,8 +36,6 @@ const useCreateUserModalForm = () => {
|
||||
: null,
|
||||
};
|
||||
|
||||
console.log(payload);
|
||||
|
||||
try {
|
||||
await registerUser(payload).unwrap();
|
||||
notification.success({
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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,
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user