сделал редактирование профиля и обновление пароля
This commit is contained in:
parent
e082bd3b45
commit
1ed36acfe2
@ -4,7 +4,7 @@ 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
|
||||
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest
|
||||
from app.domain.models import User
|
||||
from app.infrastructure.dependencies import require_auth_user
|
||||
from app.infrastructure.users_service import UsersService
|
||||
@ -24,3 +24,33 @@ 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],
|
||||
summary='Updates user data',
|
||||
description='Updates user data',
|
||||
)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_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],
|
||||
summary='Updates user data',
|
||||
description='Updates user data',
|
||||
)
|
||||
async def change_password(
|
||||
user_id: int,
|
||||
password_data: PasswordChangeRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user)
|
||||
):
|
||||
users_service = UsersService(db)
|
||||
return await users_service.change_password(user_id, password_data, user)
|
||||
|
||||
@ -22,6 +22,19 @@ class UserCreate(UserRegister):
|
||||
role_id: int = Field()
|
||||
|
||||
|
||||
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)
|
||||
email: Optional[EmailStr] = None
|
||||
birthdate: date
|
||||
|
||||
|
||||
class PasswordChangeRequest(BaseModel):
|
||||
password: str
|
||||
repeat_password: str
|
||||
|
||||
|
||||
class UserRead(BaseModel):
|
||||
id: int
|
||||
first_name: str
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.users_repository import UsersRepository
|
||||
from app.domain.entities.users import UserRead
|
||||
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest
|
||||
from app.domain.models import User
|
||||
from app.infrastructure.register_service import RegisterService
|
||||
from app.settings import Settings
|
||||
|
||||
|
||||
class UsersService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.users_repository = UsersRepository(db)
|
||||
self.settings = Settings()
|
||||
|
||||
async def get_by_id(self, user_id: int) -> UserRead:
|
||||
user = await self.users_repository.get_by_id(user_id)
|
||||
@ -18,3 +24,61 @@ class UsersService:
|
||||
)
|
||||
|
||||
return UserRead.model_validate(user)
|
||||
|
||||
async def update(self, user_id: int, user: UserUpdate, current_user: User) -> Optional[UserRead]:
|
||||
user_model = await self.users_repository.get_by_id(user_id)
|
||||
if not user_model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Пользователь не найден',
|
||||
)
|
||||
|
||||
if current_user.id != user_model.id and not current_user.role.title != self.settings.root_role_name:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Доступ запрещен',
|
||||
)
|
||||
|
||||
user_model.first_name = user.first_name
|
||||
user_model.last_name = user.last_name
|
||||
user_model.patronymic = user.patronymic
|
||||
user_model.email = user.email
|
||||
user_model.birthdate = user.birthdate
|
||||
|
||||
user_model = await self.users_repository.update(user_model)
|
||||
|
||||
return UserRead.model_validate(user_model)
|
||||
|
||||
async def change_password(
|
||||
self, user_id: int, password_data: PasswordChangeRequest, current_user: User
|
||||
) -> Optional[UserRead]:
|
||||
user_model = await self.users_repository.get_by_id(user_id)
|
||||
if not user_model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Пользователь не найден',
|
||||
)
|
||||
|
||||
if current_user.id != user_model.id and not current_user.role.title != self.settings.root_role_name:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='Доступ запрещен',
|
||||
)
|
||||
|
||||
if password_data.password != password_data.repeat_password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Пароли не совпадают',
|
||||
)
|
||||
|
||||
if not RegisterService.is_strong_password(password_data.repeat_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Пароль слишком слабый. Пароль должен содержать не менее 8 символов, включая хотя бы одну букву и одну цифру и один специальный символ.'
|
||||
)
|
||||
|
||||
user_model.set_password(password_data.password)
|
||||
|
||||
user_model = await self.users_repository.update(user_model)
|
||||
|
||||
return UserRead.model_validate(user_model)
|
||||
|
||||
@ -10,7 +10,27 @@ export const usersApi = createApi({
|
||||
getAuthenticatedUserData: builder.query({
|
||||
query: () => "/users/me/",
|
||||
}),
|
||||
updateUser: builder.mutation({
|
||||
query: ({userId, ...data}) => ({
|
||||
url: `/users/${userId}/`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["user"],
|
||||
}),
|
||||
updateUserPassword: builder.mutation({
|
||||
query: ({userId, ...data}) => ({
|
||||
url: `/users/change-password/${userId}/`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["user"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {useGetAuthenticatedUserDataQuery} = usersApi;
|
||||
export const {
|
||||
useGetAuthenticatedUserDataQuery,
|
||||
useUpdateUserMutation,
|
||||
useUpdateUserPasswordMutation,
|
||||
} = usersApi;
|
||||
@ -24,7 +24,7 @@ const AdminRoute = () => {
|
||||
return <Navigate to="/login"/>;
|
||||
}
|
||||
|
||||
if (!user.role || user.role.title !== "Администратор") {
|
||||
if (!user.role || user.role.title !== "root") {
|
||||
return <Navigate to="/"/>;
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import AdminRoute from "./AdminRoute.jsx";
|
||||
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";
|
||||
|
||||
|
||||
const AppRouter = () => (
|
||||
@ -12,7 +13,9 @@ const AppRouter = () => (
|
||||
|
||||
<Route element={<PrivateRoute/>}>
|
||||
<Route element={<MainLayout/>}>
|
||||
<Route path={"/"} element={<CoursesPage/>}/>
|
||||
<Route path={"/courses"} element={<CoursesPage/>}/>
|
||||
<Route path={"/profile"} element={<ProfilePage/>}/>
|
||||
<Route path={"*"} element={<Navigate to={"/courses"}/>}/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
@ -21,13 +21,12 @@ const MainLayout = () => {
|
||||
} = useMainLayout();
|
||||
|
||||
const menuItems = [
|
||||
getItem("Мои курсы", "/", <BookOutlined/>),
|
||||
getItem("Мои курсы", "/courses", <BookOutlined/>),
|
||||
getItem("Профиль", "/profile", <UserOutlined/>),
|
||||
getItem("Выйти", "logout", <LogoutOutlined/>),
|
||||
{type: "divider"}
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<Layout style={{minHeight: "100vh", margin: "-0.4vw"}}>
|
||||
<Sider
|
||||
|
||||
@ -34,7 +34,6 @@ const LoginPage = () => {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
</Flex>
|
||||
)
|
||||
};
|
||||
|
||||
@ -14,7 +14,7 @@ const useLoginPage = () => {
|
||||
const hasRedirected = useRef(false);
|
||||
|
||||
const pageContainerStyle = {
|
||||
paddingTop: screen.xs ? "100px" : "200px"
|
||||
paddingTop: screen.xs ? "100px" : "200px",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
117
web/src/Components/Pages/ProfilePage/ProfilePage.jsx
Normal file
117
web/src/Components/Pages/ProfilePage/ProfilePage.jsx
Normal file
@ -0,0 +1,117 @@
|
||||
import useProfilePage from "./useProfilePage.js";
|
||||
import {Button, Col, DatePicker, Divider, Form, Input, Result, Row, Typography} from "antd";
|
||||
import {UserOutlined} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
|
||||
const {Title} = Typography;
|
||||
|
||||
const ProfilePage = () => {
|
||||
const {
|
||||
userForm,
|
||||
passwordForm,
|
||||
userData,
|
||||
isLoading,
|
||||
isError,
|
||||
isUpdateUserLoading,
|
||||
isUpdateUserError,
|
||||
onFinishProfileForm,
|
||||
onFinishPasswordForm
|
||||
} = useProfilePage();
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Result
|
||||
status="error"
|
||||
title="Ошибка"
|
||||
subTitle="Произошла ошибка при загрузке данных профиля"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingIndicator/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Col>
|
||||
<Row>
|
||||
<Title>
|
||||
<UserOutlined/> Управление профилем
|
||||
</Title>
|
||||
<Divider/>
|
||||
|
||||
</Row>
|
||||
<Form name={"profile"} form={userForm} onFinish={onFinishProfileForm}>
|
||||
<Form.Item
|
||||
name="first_name"
|
||||
rules={[{required: true, message: "Введите имя"}]}
|
||||
>
|
||||
<Input placeholder={"Имя"}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="last_name"
|
||||
rules={[{required: true, message: "Введите фамилию"}]}
|
||||
>
|
||||
<Input placeholder={"Фамилия"}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="patronymic"
|
||||
>
|
||||
<Input placeholder={"Отчество"}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[{required: true, message: "Введите email"}]}
|
||||
>
|
||||
<Input placeholder={"Email"}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="birthdate"
|
||||
rules={[{required: true, message: "Введите дату рождения"}]}
|
||||
>
|
||||
<DatePicker
|
||||
maxDate={dayjs(new Date())}
|
||||
placeholder={"Дата рождения"}
|
||||
format={"DD.MM.YYYY"}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="login"
|
||||
rules={[{required: true, message: "Введите логин"}]}
|
||||
>
|
||||
<Input placeholder={"Логин"} disabled/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={isUpdateUserLoading} type="primary" htmlType="submit" block>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Divider/>
|
||||
|
||||
<Title level={3}>Смена пароля</Title>
|
||||
<Form name={"password"} form={passwordForm} onFinish={onFinishPasswordForm}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: "Введите пароль"}]}
|
||||
>
|
||||
<Input.Password placeholder={"Пароль"}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="repeat_password"
|
||||
rules={[{required: true, message: "Введите пароль"}]}
|
||||
>
|
||||
<Input.Password placeholder={"Повторите пароль"}/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={isLoading} type="primary" htmlType="submit" block>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
117
web/src/Components/Pages/ProfilePage/useProfilePage.js
Normal file
117
web/src/Components/Pages/ProfilePage/useProfilePage.js
Normal file
@ -0,0 +1,117 @@
|
||||
import {useEffect} from "react";
|
||||
import {
|
||||
useGetAuthenticatedUserDataQuery,
|
||||
useUpdateUserMutation,
|
||||
useUpdateUserPasswordMutation
|
||||
} from "../../../Api/usersApi.js";
|
||||
import {Form, notification} from "antd";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
|
||||
const useProfilePage = () => {
|
||||
const [userForm] = Form.useForm();
|
||||
const [passwordForm] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
window.document.title = "Профиль";
|
||||
}, []);
|
||||
|
||||
const {
|
||||
data: userData = {},
|
||||
isLoading: isUserDataLoading,
|
||||
isError: isUserDataError,
|
||||
} = useGetAuthenticatedUserDataQuery(undefined, {
|
||||
pollingInterval: 20000,
|
||||
});
|
||||
|
||||
const [
|
||||
updateUser,
|
||||
{
|
||||
isLoading: isUpdateUserLoading,
|
||||
isError: isUpdateUserError,
|
||||
}
|
||||
] = useUpdateUserMutation();
|
||||
|
||||
const [
|
||||
updateUserPassword,
|
||||
{
|
||||
isLoading: isUpdateUserPasswordLoading,
|
||||
isError: isUpdateUserPasswordError,
|
||||
}
|
||||
] = useUpdateUserPasswordMutation();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (userData && Object.keys(userData).length > 0) {
|
||||
const formattedValues = {
|
||||
...userData,
|
||||
birthdate: userData.birthdate ? dayjs(userData.birthdate) : null,
|
||||
};
|
||||
userForm.setFieldsValue(formattedValues);
|
||||
}
|
||||
}, [userData, userForm]);
|
||||
|
||||
const onFinishProfileForm = async () => {
|
||||
const values = userForm.getFieldsValue();
|
||||
const payload = {
|
||||
...values,
|
||||
birthdate: values.birthdate
|
||||
? values.birthdate.format("YYYY-MM-DD")
|
||||
: null,
|
||||
};
|
||||
|
||||
try {
|
||||
await updateUser({userId: userData.id, ...payload}).unwrap();
|
||||
|
||||
notification.success({
|
||||
title: "Успешно",
|
||||
description: "Данные успешно обновлены",
|
||||
placement: "topRight",
|
||||
});
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось обновить профиль.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishPasswordForm = async () => {
|
||||
const values = passwordForm.getFieldsValue();
|
||||
const payload = {
|
||||
...values,
|
||||
};
|
||||
|
||||
try {
|
||||
await updateUserPassword({userId: userData.id, ...payload}).unwrap();
|
||||
passwordForm.resetFields();
|
||||
|
||||
notification.success({
|
||||
title: "Успешно",
|
||||
description: "Пароль успешно обновлен",
|
||||
placement: "topRight",
|
||||
});
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
title: "Ошибка",
|
||||
description: error?.data?.detail || "Не удалось обновить пароль.",
|
||||
placement: "topRight",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
userForm,
|
||||
passwordForm,
|
||||
userData,
|
||||
isLoading: isUserDataLoading,
|
||||
isError: isUserDataError,
|
||||
isUpdateUserLoading,
|
||||
isUpdateUserError,
|
||||
onFinishProfileForm,
|
||||
onFinishPasswordForm,
|
||||
}
|
||||
};
|
||||
|
||||
export default useProfilePage;
|
||||
Loading…
x
Reference in New Issue
Block a user