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

This commit is contained in:
Андрей Дувакин 2025-11-28 10:05:52 +05:00
parent 1ed36acfe2
commit 12c448aef9
21 changed files with 727 additions and 94 deletions

View File

@ -10,10 +10,15 @@ class RolesRepository:
def __init__(self, db: AsyncSession) -> None: def __init__(self, db: AsyncSession) -> None:
self.db = db 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]: async def get_by_id(self, role_id: int) -> Optional[Role]:
query = ( query = (
select(Role) select(Role)
.filter_by(role_id=role_id) .filter_by(id=role_id)
) )
result = await self.db.execute(query) result = await self.db.execute(query)
return result.scalars().first() return result.scalars().first()

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import Optional, List
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -10,6 +10,11 @@ class UsersRepository:
def __init__(self, db: AsyncSession) -> None: def __init__(self, db: AsyncSession) -> None:
self.db = db 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]: async def get_by_id(self, user_id: int) -> Optional[User]:
query = ( query = (
select(User) select(User)

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.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()

View File

@ -4,14 +4,29 @@ from fastapi import APIRouter, Depends, Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db 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.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 from app.infrastructure.users_service import UsersService
users_router = APIRouter() 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( @users_router.get(
'/me/', '/me/',
response_model=Optional[UserRead], response_model=Optional[UserRead],
@ -25,6 +40,7 @@ async def get_authenticated_user_data(
users_service = UsersService(db) users_service = UsersService(db)
return await users_service.get_by_id(user.id) return await users_service.get_by_id(user.id)
@users_router.put( @users_router.put(
'/{user_id}/', '/{user_id}/',
response_model=Optional[UserRead], response_model=Optional[UserRead],
@ -40,6 +56,7 @@ async def update_user(
users_service = UsersService(db) users_service = UsersService(db)
return await users_service.update(user_id, user_data, user) return await users_service.update(user_id, user_data, user)
@users_router.post( @users_router.post(
'/change-password/{user_id}/', '/change-password/{user_id}/',
response_model=Optional[UserRead], response_model=Optional[UserRead],
@ -54,3 +71,17 @@ async def change_password(
): ):
users_service = UsersService(db) users_service = UsersService(db)
return await users_service.change_password(user_id, password_data, user) 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)

View File

@ -18,7 +18,15 @@ class UserRegister(BaseModel):
repeat_password: str = Field(min_length=8) 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() role_id: int = Field()

View 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

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import Optional, List
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -15,6 +15,15 @@ class UsersService:
self.users_repository = UsersRepository(db) self.users_repository = UsersRepository(db)
self.settings = Settings() 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: async def get_by_id(self, user_id: int) -> UserRead:
user = await self.users_repository.get_by_id(user_id) user = await self.users_repository.get_by_id(user_id)
if not user: if not user:

View File

@ -3,6 +3,7 @@ from starlette.middleware.cors import CORSMiddleware
from app.controllers.auth_router import auth_router from app.controllers.auth_router import auth_router
from app.controllers.register_router import register_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.controllers.users_router import users_router
from app.settings import Settings 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(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(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']) api_app.include_router(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
return api_app return api_app

20
web/src/Api/rolesApi.js Normal file
View 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;

View File

@ -7,6 +7,10 @@ export const usersApi = createApi({
baseQuery: baseQueryWithAuth, baseQuery: baseQueryWithAuth,
tagTypes: ["user"], tagTypes: ["user"],
endpoints: (builder) => ({ endpoints: (builder) => ({
getAllUsers: builder.query({
query: () => "/users/",
providesTags: ["user"],
}),
getAuthenticatedUserData: builder.query({ getAuthenticatedUserData: builder.query({
query: () => "/users/me/", query: () => "/users/me/",
}), }),
@ -26,11 +30,21 @@ export const usersApi = createApi({
}), }),
invalidatesTags: ["user"], invalidatesTags: ["user"],
}), }),
createUser: builder.mutation({
query: (data) => ({
url: "/users/create/",
method: "POST",
body: data,
}),
invalidatesTags: ["user"],
}),
}), }),
}); });
export const { export const {
useGetAllUsersQuery,
useGetAuthenticatedUserDataQuery, useGetAuthenticatedUserDataQuery,
useUpdateUserMutation, useUpdateUserMutation,
useUpdateUserPasswordMutation, useUpdateUserPasswordMutation,
useCreateUserMutation,
} = usersApi; } = usersApi;

View File

@ -2,6 +2,7 @@ import {Navigate, Outlet} from "react-router-dom";
import {useGetAuthenticatedUserDataQuery} from "../Api/usersApi.js"; import {useGetAuthenticatedUserDataQuery} from "../Api/usersApi.js";
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
import {Result} from "antd"; import {Result} from "antd";
import CONFIG from "../Core/сonfig.js";
const AdminRoute = () => { const AdminRoute = () => {
const { const {
@ -24,7 +25,7 @@ const AdminRoute = () => {
return <Navigate to="/login"/>; return <Navigate to="/login"/>;
} }
if (!user.role || user.role.title !== "root") { if (!user.role || user.role.title !== CONFIG.ROOT_ROLE_NAME) {
return <Navigate to="/"/>; return <Navigate to="/"/>;
} }

View File

@ -5,6 +5,7 @@ import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx";
import CoursesPage from "../Components/Pages/Courses/CoursesPage.jsx"; import CoursesPage from "../Components/Pages/Courses/CoursesPage.jsx";
import MainLayout from "../Components/Layouts/MainLayout.jsx"; import MainLayout from "../Components/Layouts/MainLayout.jsx";
import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx"; import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx";
const AppRouter = () => ( const AppRouter = () => (
@ -20,9 +21,9 @@ const AppRouter = () => (
</Route> </Route>
<Route element={<AdminRoute/>}> <Route element={<AdminRoute/>}>
{/*<Route element={<MainLayout />}>*/} <Route element={<MainLayout />}>
{/* <Route path="/admin" element={<AdminPage />} />*/} <Route path="/admin" element={<AdminPage />} />
{/*</Route>*/} </Route>
</Route> </Route>
<Route path={"*"} element={<Navigate to={"/"}/>}/> <Route path={"*"} element={<Navigate to={"/"}/>}/>

View File

@ -3,7 +3,8 @@ import {Layout, Menu} from "antd";
import CoursesPage from "../Pages/Courses/CoursesPage.jsx"; import CoursesPage from "../Pages/Courses/CoursesPage.jsx";
import LoadingIndicator from "../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import {Outlet} from "react-router-dom"; 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; const {Content, Footer, Sider} = Layout;
@ -22,11 +23,20 @@ const MainLayout = () => {
const menuItems = [ const menuItems = [
getItem("Мои курсы", "/courses", <BookOutlined/>), getItem("Мои курсы", "/courses", <BookOutlined/>),
getItem("Профиль", "/profile", <UserOutlined/>),
getItem("Выйти", "logout", <LogoutOutlined/>),
{type: "divider"} {type: "divider"}
]; ];
if (user.role.title === CONFIG.ROOT_ROLE_NAME) {
menuItems.push(
getItem("Панель администратора", "/admin", <ControlOutlined/>)
)
}
menuItems.push(
getItem("Профиль", "/profile", <UserOutlined/>),
getItem("Выйти", "logout", <LogoutOutlined/>),
)
return ( return (
<Layout style={{minHeight: "100vh", margin: "-0.4vw"}}> <Layout style={{minHeight: "100vh", margin: "-0.4vw"}}>
<Sider <Sider

View 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;

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -1,10 +1,31 @@
import useProfilePage from "./useProfilePage.js"; import useProfilePage from "./useProfilePage.js";
import {Button, Col, DatePicker, Divider, Form, Input, Result, Row, Typography} from "antd"; import {
import {UserOutlined} from "@ant-design/icons"; 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 dayjs from "dayjs";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
const {Title} = Typography; const {Title, Text} = Typography;
const ProfilePage = () => { const ProfilePage = () => {
const { const {
@ -16,15 +37,15 @@ const ProfilePage = () => {
isUpdateUserLoading, isUpdateUserLoading,
isUpdateUserError, isUpdateUserError,
onFinishProfileForm, onFinishProfileForm,
onFinishPasswordForm onFinishPasswordForm,
} = useProfilePage(); } = useProfilePage();
if (isError) { if (isError) {
return ( return (
<Result <Result
status="error" status="error"
title="Ошибка" title="Ошибка загрузки профиля"
subTitle="Произошла ошибка при загрузке данных профиля" subTitle="Не удалось загрузить данные. Попробуйте обновить страницу."
/> />
); );
} }
@ -34,83 +55,157 @@ const ProfilePage = () => {
} }
return ( return (
<Col> <Row justify="center" style={{padding: "24px 16px", minHeight: "100vh", background: "#f5f5f5"}}>
<Row> <Col xs={24} sm={22} md={20} lg={16} xl={12}>
<Title> <Card
<UserOutlined/> Управление профилем title={
<Space>
<Avatar size={40} icon={<UserOutlined/>} style={{backgroundColor: "#1890ff"}}/>
<div>
<Title level={4} style={{margin: -3}}>
{userData?.first_name} {userData?.last_name}
</Title> </Title>
<Divider/> <Text type="secondary">Роль: {userData?.role?.title || "Пользователь"}</Text>
</div>
</Row> </Space>
<Form name={"profile"} form={userForm} onFinish={onFinishProfileForm}> }
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 <Form.Item
name="first_name" name="first_name"
label="Имя"
rules={[{required: true, message: "Введите имя"}]} rules={[{required: true, message: "Введите имя"}]}
> >
<Input placeholder={"Имя"}/> <Input prefix={<UserOutlined/>} placeholder="Иван" size="large"/>
</Form.Item> </Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item <Form.Item
name="last_name" name="last_name"
label="Фамилия"
rules={[{required: true, message: "Введите фамилию"}]} rules={[{required: true, message: "Введите фамилию"}]}
> >
<Input placeholder={"Фамилия"}/> <Input prefix={<UserOutlined/>} placeholder="Иванов" size="large"/>
</Form.Item> </Form.Item>
<Form.Item </Col>
name="patronymic"
> <Col xs={24} md={12}>
<Input placeholder={"Отчество"}/> <Form.Item name="patronymic" label="Отчество">
<Input prefix={<UserOutlined/>} placeholder="Иванович" size="large"/>
</Form.Item> </Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item <Form.Item
name="email" 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> </Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item <Form.Item
name="birthdate" name="birthdate"
rules={[{required: true, message: "Введите дату рождения"}]} label="Дата рождения"
rules={[{required: true, message: "Выберите дату рождения"}]}
> >
<DatePicker <DatePicker
maxDate={dayjs(new Date())} suffixIcon={<CalendarOutlined/>}
placeholder={"Дата рождения"} format="DD.MM.YYYY"
format={"DD.MM.YYYY"} placeholder="15.03.1995"
style={{width: "100%"}}
size="large"
maxDate={dayjs()}
/> />
</Form.Item> </Form.Item>
<Form.Item </Col>
name="login"
rules={[{required: true, message: "Введите логин"}]} <Col xs={24} md={12}>
> <Form.Item name="login" label="Логин">
<Input placeholder={"Логин"} disabled/> <Input disabled prefix={<UserOutlined/>} size="large"/>
</Form.Item> </Form.Item>
<Form.Item> </Col>
<Button loading={isUpdateUserLoading} type="primary" htmlType="submit" block> </Row>
Сохранить
<Form.Item style={{marginBottom: 0}}>
<Button
type="primary"
htmlType="submit"
loading={isUpdateUserLoading}
icon={<SaveOutlined/>}
size="large"
block
>
Сохранить изменения
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </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 <Form.Item
name="password" 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> </Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item <Form.Item
name="repeat_password" 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>
<Form.Item> </Col>
<Button loading={isLoading} type="primary" htmlType="submit" block> </Row>
Сохранить
<Form.Item style={{marginBottom: 0}}>
<Button
type="primary"
htmlType="submit"
icon={<SaveOutlined/>}
size="large"
block
>
Изменить пароль
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>
</Card>
</Col> </Col>
</Row>
); );
}; };

View File

@ -1,5 +1,6 @@
const CONFIG = { const CONFIG = {
BASE_URL: import.meta.env.VITE_BASE_URL, BASE_URL: import.meta.env.VITE_BASE_URL,
ROOT_ROLE_NAME: import.meta.env.VITE_ROOT_ROLE_NAME
}; };
export default CONFIG; export default CONFIG;

View 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;

View File

@ -1,19 +1,25 @@
import {configureStore} from "@reduxjs/toolkit"; import {configureStore} from "@reduxjs/toolkit";
import authReducer from "./Slices/authSlice.js"; import authReducer from "./Slices/authSlice.js";
import usersReducer from "./Slices/usersSlice.js";
import {authApi} from "../Api/authApi.js"; import {authApi} from "../Api/authApi.js";
import {usersApi} from "../Api/usersApi.js"; import {usersApi} from "../Api/usersApi.js";
import {rolesApi} from "../Api/rolesApi.js";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
auth: authReducer, auth: authReducer,
[authApi.reducerPath]: authApi.reducer, [authApi.reducerPath]: authApi.reducer,
[usersApi.reducerPath]: usersApi.reducer users: usersReducer,
[usersApi.reducerPath]: usersApi.reducer,
[rolesApi.reducerPath]: rolesApi.reducer,
}, },
middleware: (getDefaultMiddleware) => ( middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat( getDefaultMiddleware().concat(
authApi.middleware, authApi.middleware,
usersApi.middleware, usersApi.middleware,
rolesApi.middleware,
) )
), ),
}); });