сделал редактирование профиля и обновление пароля

This commit is contained in:
Андрей Дувакин 2025-11-27 21:51:10 +05:00
parent e082bd3b45
commit 1ed36acfe2
11 changed files with 371 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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="/"/>;
}

View File

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

View File

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

View File

@ -34,7 +34,6 @@ const LoginPage = () => {
</Form.Item>
</Form>
</Col>
</Flex>
)
};

View File

@ -14,7 +14,7 @@ const useLoginPage = () => {
const hasRedirected = useRef(false);
const pageContainerStyle = {
paddingTop: screen.xs ? "100px" : "200px"
paddingTop: screen.xs ? "100px" : "200px",
};
useEffect(() => {

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

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