сделал создание пользователя
This commit is contained in:
parent
1ed36acfe2
commit
12c448aef9
@ -10,10 +10,15 @@ class RolesRepository:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.db = db
|
||||
|
||||
async def get_all(self) -> list[Role]:
|
||||
query = select(Role)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_id(self, role_id: int) -> Optional[Role]:
|
||||
query = (
|
||||
select(Role)
|
||||
.filter_by(role_id=role_id)
|
||||
.filter_by(id=role_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@ -10,6 +10,11 @@ class UsersRepository:
|
||||
def __init__(self, db: AsyncSession) -> None:
|
||||
self.db = db
|
||||
|
||||
async def get_all(self) -> List[User]:
|
||||
query = select(User)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_id(self, user_id: int) -> Optional[User]:
|
||||
query = (
|
||||
select(User)
|
||||
|
||||
26
api/app/controllers/roles_router.py
Normal file
26
api/app/controllers/roles_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.roles import RoleRead
|
||||
from app.domain.models import User
|
||||
from app.infrastructure.dependencies import require_admin
|
||||
from app.infrastructure.roles_service import RolesService
|
||||
|
||||
roles_router = APIRouter()
|
||||
|
||||
|
||||
@roles_router.get(
|
||||
'/',
|
||||
response_model=List[RoleRead],
|
||||
summary='Returns all roles',
|
||||
description='Returns all roles',
|
||||
)
|
||||
async def get_all_roles(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
roles_service = RolesService(db)
|
||||
return await roles_service.get_all()
|
||||
@ -4,14 +4,29 @@ from fastapi import APIRouter, Depends, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.session import get_db
|
||||
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest
|
||||
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest, UserCreate
|
||||
from app.domain.models import User
|
||||
from app.infrastructure.dependencies import require_auth_user
|
||||
from app.infrastructure.dependencies import require_auth_user, require_admin
|
||||
from app.infrastructure.register_service import RegisterService
|
||||
from app.infrastructure.users_service import UsersService
|
||||
|
||||
users_router = APIRouter()
|
||||
|
||||
|
||||
@users_router.get(
|
||||
'/',
|
||||
response_model=List[UserRead],
|
||||
summary='Return all users',
|
||||
description='Return all users',
|
||||
)
|
||||
async def get_all_users(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_admin)
|
||||
):
|
||||
users_service = UsersService(db)
|
||||
return await users_service.get_all()
|
||||
|
||||
|
||||
@users_router.get(
|
||||
'/me/',
|
||||
response_model=Optional[UserRead],
|
||||
@ -25,6 +40,7 @@ async def get_authenticated_user_data(
|
||||
users_service = UsersService(db)
|
||||
return await users_service.get_by_id(user.id)
|
||||
|
||||
|
||||
@users_router.put(
|
||||
'/{user_id}/',
|
||||
response_model=Optional[UserRead],
|
||||
@ -40,6 +56,7 @@ async def update_user(
|
||||
users_service = UsersService(db)
|
||||
return await users_service.update(user_id, user_data, user)
|
||||
|
||||
|
||||
@users_router.post(
|
||||
'/change-password/{user_id}/',
|
||||
response_model=Optional[UserRead],
|
||||
@ -54,3 +71,17 @@ 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],
|
||||
summary='Creates a new user',
|
||||
description='Creates a new user',
|
||||
)
|
||||
async def create_user(
|
||||
user: UserCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(require_admin)
|
||||
):
|
||||
register_service = RegisterService(db)
|
||||
return await register_service.create_user(user)
|
||||
|
||||
@ -18,7 +18,15 @@ class UserRegister(BaseModel):
|
||||
repeat_password: str = Field(min_length=8)
|
||||
|
||||
|
||||
class UserCreate(UserRegister):
|
||||
class UserCreate(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
|
||||
password: str = Field(min_length=8)
|
||||
repeat_password: str = Field(min_length=8)
|
||||
role_id: int = Field()
|
||||
|
||||
|
||||
|
||||
20
api/app/infrastructure/roles_service.py
Normal file
20
api/app/infrastructure/roles_service.py
Normal file
@ -0,0 +1,20 @@
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.roles_repository import RolesRepository
|
||||
from app.domain.entities.roles import RoleRead
|
||||
|
||||
|
||||
class RolesService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.roles_repository = RolesRepository(db)
|
||||
|
||||
async def get_all(self) -> List[RoleRead]:
|
||||
roles = await self.roles_repository.get_all()
|
||||
response = []
|
||||
|
||||
for role in roles:
|
||||
response.append(RoleRead.model_validate(role))
|
||||
|
||||
return response
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@ -15,6 +15,15 @@ class UsersService:
|
||||
self.users_repository = UsersRepository(db)
|
||||
self.settings = Settings()
|
||||
|
||||
async def get_all(self) -> List[UserRead]:
|
||||
users = await self.users_repository.get_all()
|
||||
response = []
|
||||
|
||||
for user in users:
|
||||
response.append(UserRead.model_validate(user))
|
||||
|
||||
return response
|
||||
|
||||
async def get_by_id(self, user_id: int) -> UserRead:
|
||||
user = await self.users_repository.get_by_id(user_id)
|
||||
if not user:
|
||||
|
||||
@ -3,6 +3,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.users_router import users_router
|
||||
from app.settings import Settings
|
||||
|
||||
@ -21,6 +22,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(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
|
||||
|
||||
return api_app
|
||||
|
||||
20
web/src/Api/rolesApi.js
Normal file
20
web/src/Api/rolesApi.js
Normal file
@ -0,0 +1,20 @@
|
||||
import {baseQueryWithAuth} from "./baseQuery.js";
|
||||
import {createApi} from "@reduxjs/toolkit/query/react";
|
||||
|
||||
|
||||
export const rolesApi = createApi({
|
||||
reducerPath: "rolesApi",
|
||||
baseQuery: baseQueryWithAuth,
|
||||
tagTypes: ["role"],
|
||||
endpoints: (builder) => ({
|
||||
getAllRoles: builder.query({
|
||||
query: () => ({
|
||||
url: "/roles/",
|
||||
method: "GET",
|
||||
}),
|
||||
providesTags: ["role"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {useGetAllRolesQuery} = rolesApi;
|
||||
@ -7,6 +7,10 @@ export const usersApi = createApi({
|
||||
baseQuery: baseQueryWithAuth,
|
||||
tagTypes: ["user"],
|
||||
endpoints: (builder) => ({
|
||||
getAllUsers: builder.query({
|
||||
query: () => "/users/",
|
||||
providesTags: ["user"],
|
||||
}),
|
||||
getAuthenticatedUserData: builder.query({
|
||||
query: () => "/users/me/",
|
||||
}),
|
||||
@ -26,11 +30,21 @@ export const usersApi = createApi({
|
||||
}),
|
||||
invalidatesTags: ["user"],
|
||||
}),
|
||||
createUser: builder.mutation({
|
||||
query: (data) => ({
|
||||
url: "/users/create/",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["user"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetAllUsersQuery,
|
||||
useGetAuthenticatedUserDataQuery,
|
||||
useUpdateUserMutation,
|
||||
useUpdateUserPasswordMutation,
|
||||
useCreateUserMutation,
|
||||
} = usersApi;
|
||||
@ -2,6 +2,7 @@ import {Navigate, Outlet} from "react-router-dom";
|
||||
import {useGetAuthenticatedUserDataQuery} from "../Api/usersApi.js";
|
||||
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import {Result} from "antd";
|
||||
import CONFIG from "../Core/сonfig.js";
|
||||
|
||||
const AdminRoute = () => {
|
||||
const {
|
||||
@ -24,7 +25,7 @@ const AdminRoute = () => {
|
||||
return <Navigate to="/login"/>;
|
||||
}
|
||||
|
||||
if (!user.role || user.role.title !== "root") {
|
||||
if (!user.role || user.role.title !== CONFIG.ROOT_ROLE_NAME) {
|
||||
return <Navigate to="/"/>;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx";
|
||||
import CoursesPage from "../Components/Pages/Courses/CoursesPage.jsx";
|
||||
import MainLayout from "../Components/Layouts/MainLayout.jsx";
|
||||
import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
|
||||
import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx";
|
||||
|
||||
|
||||
const AppRouter = () => (
|
||||
@ -20,9 +21,9 @@ const AppRouter = () => (
|
||||
</Route>
|
||||
|
||||
<Route element={<AdminRoute/>}>
|
||||
{/*<Route element={<MainLayout />}>*/}
|
||||
{/* <Route path="/admin" element={<AdminPage />} />*/}
|
||||
{/*</Route>*/}
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path={"*"} element={<Navigate to={"/"}/>}/>
|
||||
|
||||
@ -3,7 +3,8 @@ import {Layout, Menu} from "antd";
|
||||
import CoursesPage from "../Pages/Courses/CoursesPage.jsx";
|
||||
import LoadingIndicator from "../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import {Outlet} from "react-router-dom";
|
||||
import {BookOutlined, LogoutOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import {BookOutlined, ControlOutlined, LogoutOutlined, UserOutlined} from "@ant-design/icons";
|
||||
import CONFIG from "../../Core/сonfig.js";
|
||||
|
||||
const {Content, Footer, Sider} = Layout;
|
||||
|
||||
@ -22,11 +23,20 @@ const MainLayout = () => {
|
||||
|
||||
const menuItems = [
|
||||
getItem("Мои курсы", "/courses", <BookOutlined/>),
|
||||
getItem("Профиль", "/profile", <UserOutlined/>),
|
||||
getItem("Выйти", "logout", <LogoutOutlined/>),
|
||||
{type: "divider"}
|
||||
];
|
||||
|
||||
if (user.role.title === CONFIG.ROOT_ROLE_NAME) {
|
||||
menuItems.push(
|
||||
getItem("Панель администратора", "/admin", <ControlOutlined/>)
|
||||
)
|
||||
}
|
||||
|
||||
menuItems.push(
|
||||
getItem("Профиль", "/profile", <UserOutlined/>),
|
||||
getItem("Выйти", "logout", <LogoutOutlined/>),
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout style={{minHeight: "100vh", margin: "-0.4vw"}}>
|
||||
<Sider
|
||||
|
||||
104
web/src/Components/Pages/AdminPage/AdminPage.jsx
Normal file
104
web/src/Components/Pages/AdminPage/AdminPage.jsx
Normal file
@ -0,0 +1,104 @@
|
||||
import {Button, Col, Flex, FloatButton, Input, Result, Row, Table, Tooltip, Typography} from "antd";
|
||||
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";
|
||||
|
||||
const {Title} = Typography;
|
||||
|
||||
const AdminPage = () => {
|
||||
const {
|
||||
handleSelectUserToEdit,
|
||||
rolesData,
|
||||
filteredUsers,
|
||||
handleSearch,
|
||||
isLoading,
|
||||
isError,
|
||||
openCreateModal,
|
||||
} = useAdminPage();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: "Фамилия",
|
||||
dataIndex: "last_name",
|
||||
key: "lastName",
|
||||
sorter: (a, b) => a.last_name.localeCompare(b.last_name),
|
||||
},
|
||||
{
|
||||
title: "Имя",
|
||||
dataIndex: "first_name",
|
||||
key: "firstName",
|
||||
sorter: (a, b) => a.first_name.localeCompare(b.first_name),
|
||||
},
|
||||
{
|
||||
title: "Отчество",
|
||||
dataIndex: "patronymic",
|
||||
key: "patronymic",
|
||||
},
|
||||
{
|
||||
title: "Роль",
|
||||
dataIndex: ["role", "title"],
|
||||
key: "role",
|
||||
filters: rolesData.map(role => ({text: role.title, value: role.title})),
|
||||
onFilter: (value, record) => record.role.title === value,
|
||||
},
|
||||
{
|
||||
title: "Статус",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
title: "Действия",
|
||||
key: "actions",
|
||||
render: (_, record) => (
|
||||
<Button type="link" onClick={() => handleSelectUserToEdit(record)}>
|
||||
Редактировать
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (isError) {
|
||||
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных"/>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingIndicator/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col style={{width: '100%'}}>
|
||||
<Title level={1}>
|
||||
<ControlOutlined/> Панель администратора
|
||||
</Title>
|
||||
<Flex vertical>
|
||||
<Input
|
||||
placeholder="Введите фамилию, имя или отчество"
|
||||
style={{marginBottom: 12, width: "100%"}}
|
||||
allowClear
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredUsers}
|
||||
rowKey="id"
|
||||
pagination={{pageSize: 10}}
|
||||
/>
|
||||
</Flex>
|
||||
<Tooltip title="Добавить пользователя">
|
||||
<FloatButton onClick={openCreateModal} icon={<PlusOutlined/>} type={"primary"}/>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
|
||||
<CreateUserModalForm/>
|
||||
</Row>
|
||||
)
|
||||
};
|
||||
|
||||
export default AdminPage;
|
||||
@ -0,0 +1,108 @@
|
||||
import {Button, DatePicker, Form, Input, Modal, Select, Tooltip, Typography} from "antd";
|
||||
import useCreateUserModalForm from "./useCreateUserModalForm.js";
|
||||
import {CalendarOutlined, InfoCircleOutlined} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
||||
const CreateUserModalForm = () => {
|
||||
const {
|
||||
modalVisible,
|
||||
handleCancel,
|
||||
handleFinish,
|
||||
form,
|
||||
roles,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useCreateUserModalForm();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Создать пользователя"
|
||||
open={modalVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
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/>
|
||||
</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>
|
||||
<Tooltip
|
||||
title="Пароль должен содержать не менее 8 символов, включая хотя бы одну букву и одну цифру и один специальный символ"
|
||||
>
|
||||
<Typography.Title level={3} style={{width: 30}}>
|
||||
<InfoCircleOutlined/>
|
||||
</Typography.Title>
|
||||
</Tooltip>
|
||||
<Form.Item label="Пароль" name="password" rules={[{required: true, message: "Введите пароль"}]}>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Подтвердите пароль" name="repeat_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={isLoading}>
|
||||
Создать
|
||||
</Button>
|
||||
<Button onClick={handleCancel} style={{marginLeft: 8}}>
|
||||
Отмена
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
};
|
||||
|
||||
export default CreateUserModalForm;
|
||||
@ -0,0 +1,79 @@
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {Form, notification} from "antd";
|
||||
import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../../Redux/Slices/usersSlice.js";
|
||||
import {useGetAllRolesQuery} from "../../../../Api/rolesApi.js";
|
||||
import {useCreateUserMutation} from "../../../../Api/usersApi.js";
|
||||
|
||||
|
||||
const useCreateUserModalForm = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const {
|
||||
openModalCreateUser
|
||||
} = useSelector(state => state.users);
|
||||
|
||||
const modalVisible = openModalCreateUser;
|
||||
|
||||
const handleCancel = () => {
|
||||
dispatch(setOpenModalCreateUser(false));
|
||||
};
|
||||
|
||||
const [
|
||||
registerUser,
|
||||
{
|
||||
isLoading: isLoadingRegister,
|
||||
isError: isErrorRegister,
|
||||
}
|
||||
] = useCreateUserMutation();
|
||||
|
||||
const handleFinish = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
const payload = {
|
||||
...values,
|
||||
birthdate: values.birthdate
|
||||
? values.birthdate.format("YYYY-MM-DD")
|
||||
: null,
|
||||
};
|
||||
|
||||
console.log(payload);
|
||||
|
||||
try {
|
||||
await registerUser(payload).unwrap();
|
||||
notification.success({
|
||||
title: "Пользователь зарегистрирован",
|
||||
description: "Пользователь успешно зарегистрирован",
|
||||
placement: "topRight",
|
||||
});
|
||||
form.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,
|
||||
form,
|
||||
roles,
|
||||
isLoading: isLoadingRoles,
|
||||
isError: isErrorRoles,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateUserModalForm;
|
||||
65
web/src/Components/Pages/AdminPage/useAdminPage.js
Normal file
65
web/src/Components/Pages/AdminPage/useAdminPage.js
Normal file
@ -0,0 +1,65 @@
|
||||
import {useGetAllUsersQuery} from "../../../Api/usersApi.js";
|
||||
import {useGetAllRolesQuery} from "../../../Api/rolesApi.js";
|
||||
import {useMemo, useState} from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../Redux/Slices/usersSlice.js";
|
||||
|
||||
|
||||
const useAdminPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [
|
||||
searchString,
|
||||
setSearchString,
|
||||
] = useState("");
|
||||
|
||||
const {
|
||||
data: usersData = [],
|
||||
isLoading: usersIsLoading,
|
||||
isError: usersIsError,
|
||||
} = useGetAllUsersQuery(undefined, {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
const {
|
||||
data: rolesData = [],
|
||||
isLoading: rolesIsLoading,
|
||||
isError: rolesIsError,
|
||||
} = useGetAllRolesQuery(undefined, {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
const handleSearch = (e) => {
|
||||
setSearchString(e.target.value);
|
||||
};
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
return usersData.filter((user) => {
|
||||
return Object.entries(user).some(([key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
return value.toString().toLowerCase().includes(searchString.toLowerCase());
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [usersData, searchString]);
|
||||
|
||||
const handleSelectUserToEdit = (user) => {
|
||||
dispatch(setSelectedUserToUpdate(user));
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
dispatch(setOpenModalCreateUser(true));
|
||||
};
|
||||
|
||||
return {
|
||||
handleSelectUserToEdit,
|
||||
rolesData,
|
||||
filteredUsers,
|
||||
handleSearch,
|
||||
isLoading: usersIsLoading | rolesIsLoading,
|
||||
isError: usersIsError | rolesIsError,
|
||||
openCreateModal,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAdminPage;
|
||||
@ -1,10 +1,31 @@
|
||||
import useProfilePage from "./useProfilePage.js";
|
||||
import {Button, Col, DatePicker, Divider, Form, Input, Result, Row, Typography} from "antd";
|
||||
import {UserOutlined} from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Result,
|
||||
Row,
|
||||
Typography,
|
||||
Avatar,
|
||||
Space,
|
||||
Divider,
|
||||
Upload,
|
||||
} from "antd";
|
||||
import {
|
||||
UserOutlined,
|
||||
MailOutlined,
|
||||
LockOutlined,
|
||||
CalendarOutlined,
|
||||
SaveOutlined,
|
||||
UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
|
||||
const {Title} = Typography;
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
const ProfilePage = () => {
|
||||
const {
|
||||
@ -16,15 +37,15 @@ const ProfilePage = () => {
|
||||
isUpdateUserLoading,
|
||||
isUpdateUserError,
|
||||
onFinishProfileForm,
|
||||
onFinishPasswordForm
|
||||
onFinishPasswordForm,
|
||||
} = useProfilePage();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="Ошибка"
|
||||
subTitle="Произошла ошибка при загрузке данных профиля"
|
||||
title="Ошибка загрузки профиля"
|
||||
subTitle="Не удалось загрузить данные. Попробуйте обновить страницу."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -34,83 +55,157 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Col>
|
||||
<Row>
|
||||
<Title>
|
||||
<UserOutlined/> Управление профилем
|
||||
<Row justify="center" style={{padding: "24px 16px", minHeight: "100vh", background: "#f5f5f5"}}>
|
||||
<Col xs={24} sm={22} md={20} lg={16} xl={12}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<Avatar size={40} icon={<UserOutlined/>} style={{backgroundColor: "#1890ff"}}/>
|
||||
<div>
|
||||
<Title level={4} style={{margin: -3}}>
|
||||
{userData?.first_name} {userData?.last_name}
|
||||
</Title>
|
||||
<Divider/>
|
||||
|
||||
</Row>
|
||||
<Form name={"profile"} form={userForm} onFinish={onFinishProfileForm}>
|
||||
<Text type="secondary">Роль: {userData?.role?.title || "Пользователь"}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
style={{borderRadius: 12, boxShadow: "0 4px 12px rgba(0,0,0,0.05)", marginBottom: 24}}
|
||||
>
|
||||
<Form form={userForm} layout="vertical" onFinish={onFinishProfileForm}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="first_name"
|
||||
label="Имя"
|
||||
rules={[{required: true, message: "Введите имя"}]}
|
||||
>
|
||||
<Input placeholder={"Имя"}/>
|
||||
<Input prefix={<UserOutlined/>} placeholder="Иван" size="large"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="last_name"
|
||||
label="Фамилия"
|
||||
rules={[{required: true, message: "Введите фамилию"}]}
|
||||
>
|
||||
<Input placeholder={"Фамилия"}/>
|
||||
<Input prefix={<UserOutlined/>} placeholder="Иванов" size="large"/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="patronymic"
|
||||
>
|
||||
<Input placeholder={"Отчество"}/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="patronymic" label="Отчество">
|
||||
<Input prefix={<UserOutlined/>} placeholder="Иванович" size="large"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[{required: true, message: "Введите email"}]}
|
||||
label="Email"
|
||||
rules={[{required: true, type: "email", message: "Введите корректный email"}]}
|
||||
>
|
||||
<Input placeholder={"Email"}/>
|
||||
<Input prefix={<MailOutlined/>} placeholder="ivan@example.com" size="large"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="birthdate"
|
||||
rules={[{required: true, message: "Введите дату рождения"}]}
|
||||
label="Дата рождения"
|
||||
rules={[{required: true, message: "Выберите дату рождения"}]}
|
||||
>
|
||||
<DatePicker
|
||||
maxDate={dayjs(new Date())}
|
||||
placeholder={"Дата рождения"}
|
||||
format={"DD.MM.YYYY"}
|
||||
suffixIcon={<CalendarOutlined/>}
|
||||
format="DD.MM.YYYY"
|
||||
placeholder="15.03.1995"
|
||||
style={{width: "100%"}}
|
||||
size="large"
|
||||
maxDate={dayjs()}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="login"
|
||||
rules={[{required: true, message: "Введите логин"}]}
|
||||
>
|
||||
<Input placeholder={"Логин"} disabled/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="login" label="Логин">
|
||||
<Input disabled prefix={<UserOutlined/>} size="large"/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={isUpdateUserLoading} type="primary" htmlType="submit" block>
|
||||
Сохранить
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item style={{marginBottom: 0}}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isUpdateUserLoading}
|
||||
icon={<SaveOutlined/>}
|
||||
size="large"
|
||||
block
|
||||
>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Divider/>
|
||||
</Card>
|
||||
|
||||
<Title level={3}>Смена пароля</Title>
|
||||
<Form name={"password"} form={passwordForm} onFinish={onFinishPasswordForm}>
|
||||
{/* Карточка смены пароля */}
|
||||
<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}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: "Введите пароль"}]}
|
||||
label="Новый пароль"
|
||||
rules={[
|
||||
{required: true, message: "Введите новый пароль"},
|
||||
{min: 8, message: "Пароль должен быть не менее 8 символов"},
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder={"Пароль"}/>
|
||||
<Input.Password prefix={<LockOutlined/>} placeholder="••••••••" size="large"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="repeat_password"
|
||||
rules={[{required: true, message: "Введите пароль"}]}
|
||||
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 placeholder={"Повторите пароль"}/>
|
||||
<Input.Password prefix={<LockOutlined/>} placeholder="••••••••" size="large"/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={isLoading} type="primary" htmlType="submit" block>
|
||||
Сохранить
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item style={{marginBottom: 0}}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined/>}
|
||||
size="large"
|
||||
block
|
||||
>
|
||||
Изменить пароль
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const CONFIG = {
|
||||
BASE_URL: import.meta.env.VITE_BASE_URL,
|
||||
ROOT_ROLE_NAME: import.meta.env.VITE_ROOT_ROLE_NAME
|
||||
};
|
||||
|
||||
export default CONFIG;
|
||||
23
web/src/Redux/Slices/usersSlice.js
Normal file
23
web/src/Redux/Slices/usersSlice.js
Normal file
@ -0,0 +1,23 @@
|
||||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
selectedUserToUpdate: null,
|
||||
openModalCreateUser: false,
|
||||
};
|
||||
|
||||
const usersSlice = createSlice({
|
||||
name: "users",
|
||||
initialState,
|
||||
reducers: {
|
||||
setSelectedUserToUpdate(state, action) {
|
||||
state.selectedUserToUpdate = action.payload;
|
||||
},
|
||||
setOpenModalCreateUser(state, action) {
|
||||
state.openModalCreateUser = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {setSelectedUserToUpdate, setOpenModalCreateUser} = usersSlice.actions;
|
||||
|
||||
export default usersSlice.reducer;
|
||||
@ -1,19 +1,25 @@
|
||||
import {configureStore} from "@reduxjs/toolkit";
|
||||
import authReducer from "./Slices/authSlice.js";
|
||||
import usersReducer from "./Slices/usersSlice.js";
|
||||
import {authApi} from "../Api/authApi.js";
|
||||
import {usersApi} from "../Api/usersApi.js";
|
||||
import {rolesApi} from "../Api/rolesApi.js";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
[authApi.reducerPath]: authApi.reducer,
|
||||
|
||||
[usersApi.reducerPath]: usersApi.reducer
|
||||
users: usersReducer,
|
||||
[usersApi.reducerPath]: usersApi.reducer,
|
||||
|
||||
[rolesApi.reducerPath]: rolesApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => (
|
||||
getDefaultMiddleware().concat(
|
||||
authApi.middleware,
|
||||
usersApi.middleware,
|
||||
rolesApi.middleware,
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user