сделал редактирование профиля и обновление пароля
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 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
|
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest
|
||||||
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
|
||||||
from app.infrastructure.users_service import UsersService
|
from app.infrastructure.users_service import UsersService
|
||||||
@ -24,3 +24,33 @@ 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(
|
||||||
|
'/{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()
|
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):
|
class UserRead(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
first_name: str
|
first_name: str
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.application.users_repository import UsersRepository
|
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:
|
class UsersService:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.users_repository = UsersRepository(db)
|
self.users_repository = UsersRepository(db)
|
||||||
|
self.settings = Settings()
|
||||||
|
|
||||||
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)
|
||||||
@ -18,3 +24,61 @@ class UsersService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return UserRead.model_validate(user)
|
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({
|
getAuthenticatedUserData: builder.query({
|
||||||
query: () => "/users/me/",
|
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"/>;
|
return <Navigate to="/login"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.role || user.role.title !== "Администратор") {
|
if (!user.role || user.role.title !== "root") {
|
||||||
return <Navigate to="/"/>;
|
return <Navigate to="/"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import AdminRoute from "./AdminRoute.jsx";
|
|||||||
import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx";
|
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";
|
||||||
|
|
||||||
|
|
||||||
const AppRouter = () => (
|
const AppRouter = () => (
|
||||||
@ -12,7 +13,9 @@ const AppRouter = () => (
|
|||||||
|
|
||||||
<Route element={<PrivateRoute/>}>
|
<Route element={<PrivateRoute/>}>
|
||||||
<Route element={<MainLayout/>}>
|
<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>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
@ -21,13 +21,12 @@ const MainLayout = () => {
|
|||||||
} = useMainLayout();
|
} = useMainLayout();
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
getItem("Мои курсы", "/", <BookOutlined/>),
|
getItem("Мои курсы", "/courses", <BookOutlined/>),
|
||||||
getItem("Профиль", "/profile", <UserOutlined/>),
|
getItem("Профиль", "/profile", <UserOutlined/>),
|
||||||
getItem("Выйти", "logout", <LogoutOutlined/>),
|
getItem("Выйти", "logout", <LogoutOutlined/>),
|
||||||
{type: "divider"}
|
{type: "divider"}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{minHeight: "100vh", margin: "-0.4vw"}}>
|
<Layout style={{minHeight: "100vh", margin: "-0.4vw"}}>
|
||||||
<Sider
|
<Sider
|
||||||
|
|||||||
@ -34,7 +34,6 @@ const LoginPage = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const useLoginPage = () => {
|
|||||||
const hasRedirected = useRef(false);
|
const hasRedirected = useRef(false);
|
||||||
|
|
||||||
const pageContainerStyle = {
|
const pageContainerStyle = {
|
||||||
paddingTop: screen.xs ? "100px" : "200px"
|
paddingTop: screen.xs ? "100px" : "200px",
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
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