From 9abe8fe6f60892d20aa49e57888bce0dd639d9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=BD=D0=BD=D0=B0=20=D0=A8=D0=BF=D0=BE=D0=BD=D1=8C?= =?UTF-8?q?=D0=BA=D0=B0?= Date: Wed, 17 Jun 2026 14:44:42 +0300 Subject: [PATCH] implement User password reset ability --- src/app/api/user/user.api.ts | 64 ++++---- .../ForgotPasswordModal.codal.tsx | 45 ++++++ .../ForgotPasswordModal.styles.scss | 13 ++ .../common/constants/api-routes.constants.ts | 4 +- .../constants/frontend-routes.constants.ts | 1 + src/app/router/Routes.tsx | 2 + src/app/stores/modal-store.ts | 2 + .../LoginPage/LoginPage.component.tsx | 99 ++++++------ .../ResetPasswordPage.component.tsx | 84 ++++++++++ .../ResetPasswordPage.styles.scss | 144 ++++++++++++++++++ src/models/user/user.model.ts | 9 ++ 11 files changed, 385 insertions(+), 82 deletions(-) create mode 100644 src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.codal.tsx create mode 100644 src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.styles.scss create mode 100644 src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.component.tsx create mode 100644 src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.styles.scss diff --git a/src/app/api/user/user.api.ts b/src/app/api/user/user.api.ts index aba34d5..1e213a7 100644 --- a/src/app/api/user/user.api.ts +++ b/src/app/api/user/user.api.ts @@ -1,4 +1,5 @@ import Agent from '@api/agent.api'; +import { ResetPasswordRequest,ForgotPasswordRequest } from '@models/user/user.model'; import { API_ROUTES } from '@/app/common/constants/api-routes.constants'; import { @@ -6,33 +7,44 @@ import { UserLoginRequest, UserLoginResponce, } from '@/models/user/user.model'; - const UserApi = { - login: (loginParams: UserLoginRequest) => - Agent.post( - API_ROUTES.USERS.LOGIN, - loginParams, - ), +const UserApi = { + login: (loginParams: UserLoginRequest) => + Agent.post( + API_ROUTES.USERS.LOGIN, + loginParams, + ), - refreshToken: (token: RefreshTokenRequest) => - Agent.post( - API_ROUTES.USERS.REFRESH_TOKEN, - token, - ), + refreshToken: (token: RefreshTokenRequest) => + Agent.post( + API_ROUTES.USERS.REFRESH_TOKEN, + token, + ), - adminLogin: (loginParams: UserLoginRequest) => - Agent.post( - API_ROUTES.ADMIN_AUTHORIZATION.LOGIN, - loginParams, - ), + adminLogin: (loginParams: UserLoginRequest) => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.LOGIN, + loginParams, + ), - adminRefreshToken: (token: RefreshTokenRequest) => - Agent.post( - API_ROUTES.ADMIN_AUTHORIZATION.REFRESH_TOKEN, - token, - ), - adminLogout: () => - Agent.post( - API_ROUTES.ADMIN_AUTHORIZATION.LOGOUT, {}, - ), - }; + adminRefreshToken: (token: RefreshTokenRequest) => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.REFRESH_TOKEN, + token, + ), + adminLogout: () => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.LOGOUT, {}, + ), + + forgotPassword: (forgotParams: ForgotPasswordRequest) => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.FORGOT_PASSWORD, + forgotParams, + ), + resetPassword: (resetParams: ResetPasswordRequest) => + Agent.post( + API_ROUTES.ADMIN_AUTHORIZATION.RESET_PASSWORD, + resetParams, + ), +}; export default UserApi; diff --git a/src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.codal.tsx b/src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.codal.tsx new file mode 100644 index 0000000..850feda --- /dev/null +++ b/src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.codal.tsx @@ -0,0 +1,45 @@ +import './ForgotPasswordModal.styles.scss'; + +import { observer } from 'mobx-react-lite'; +import { Button, Modal, Form, Input, message } from 'antd'; +import { useModalContext } from '@stores/root-store'; +import UserApi from '@api/user/user.api'; + +const ForgotPasswordModal = () => { + const { modalStore: { setModal, modalsState: { forgotPassword } } } = useModalContext(); + + const onFinish = async (values: { email: string }) => { + try { + await UserApi.forgotPassword({ email: values.email }); + message.success('Лист для скидання пароля відправлено!'); + setModal('forgotPassword'); + } catch { + message.error('Помилка при відправці запиту'); + } + }; + + return ( + setModal('forgotPassword')} + footer={null} + className="forgotPasswordModal" + > +
+ + + + +
+
+ ); +}; + +export default observer(ForgotPasswordModal); \ No newline at end of file diff --git a/src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.styles.scss b/src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.styles.scss new file mode 100644 index 0000000..d404b23 --- /dev/null +++ b/src/app/common/components/modals/ForgotPasswordModal/ForgotPasswordModal.styles.scss @@ -0,0 +1,13 @@ +@use "@sass/variables/_variables.colors.scss" as c; + +.forgotPasswordModal { + .ant-btn-primary { + background-color: c.$accented-blue-color; + border-color: c.$accented-blue-color; + + &:hover { + background-color: c.$dark-blue-color; + border-color: c.$dark-blue-color; + } + } +} \ No newline at end of file diff --git a/src/app/common/constants/api-routes.constants.ts b/src/app/common/constants/api-routes.constants.ts index 3de91c9..2c916c2 100644 --- a/src/app/common/constants/api-routes.constants.ts +++ b/src/app/common/constants/api-routes.constants.ts @@ -228,6 +228,8 @@ export const API_ROUTES = { ADMIN_AUTHORIZATION: { LOGIN: 'auth/login', REFRESH_TOKEN: 'auth/refresh-token', - LOGOUT: 'auth/logout' + LOGOUT: 'auth/logout', + RESET_PASSWORD:'auth/reset-password', + FORGOT_PASSWORD:'auth/forgot-password', }, }; diff --git a/src/app/common/constants/frontend-routes.constants.ts b/src/app/common/constants/frontend-routes.constants.ts index a979636..82db635 100644 --- a/src/app/common/constants/frontend-routes.constants.ts +++ b/src/app/common/constants/frontend-routes.constants.ts @@ -8,6 +8,7 @@ const FRONTEND_ROUTES = { DICTIONARY: '/admin-panel/dictionary', FOR_FANS: '/admin-panel/for-fans', LOGIN: '/admin-panel/login', + RESET_PASSWORD: '/admin-panel/reset-password', TEAM: '/admin-panel/team', ANALYTICS: '/admin-panel/analytics', NEWS: '/admin-panel/news', diff --git a/src/app/router/Routes.tsx b/src/app/router/Routes.tsx index 963ef57..aba14f9 100644 --- a/src/app/router/Routes.tsx +++ b/src/app/router/Routes.tsx @@ -20,6 +20,7 @@ import Partners from "@/features/AdminPage/PartnersPage/Partners.component"; import Streetcodes from "@/features/AdminPage/StreetcodesPage/Streetcodes.component"; import TeamPage from "@/features/AdminPage/TeamPage/TeamPage.component"; import VacanciesPage from "@/features/AdminPage/VacanciesPage/VacanciesPage.component"; +import ResetPasswordPage from "@/features/AdminPage/ResetPasswordPage/ResetPasswordPage.component"; import StreetcodeCatalog from "@/features/StreetcodeCatalogPage/StreetcodeCatalog.component"; const router = createBrowserRouter( @@ -38,6 +39,7 @@ const router = createBrowserRouter( } /> } /> } /> + } /> } /> diff --git a/src/app/stores/modal-store.ts b/src/app/stores/modal-store.ts index 8a1d6c9..fb76e52 100644 --- a/src/app/stores/modal-store.ts +++ b/src/app/stores/modal-store.ts @@ -41,6 +41,7 @@ interface ModalList { adminFacts: ModalState; adminChronology: ModalState; statistics: ModalState; + forgotPassword: ModalState; } export default class ModalStore { @@ -63,6 +64,7 @@ export default class ModalStore { adminFacts: DefaultModalState, adminChronology: DefaultModalState, statistics: DefaultModalState, + forgotPassword: DefaultModalState, }; public isPageDimmed = false; diff --git a/src/features/AdminPage/LoginPage/LoginPage.component.tsx b/src/features/AdminPage/LoginPage/LoginPage.component.tsx index 5958d78..b4da825 100644 --- a/src/features/AdminPage/LoginPage/LoginPage.component.tsx +++ b/src/features/AdminPage/LoginPage/LoginPage.component.tsx @@ -1,31 +1,38 @@ import './LoginPage.styles.scss'; import { useState } from 'react'; -import { Button, Checkbox, Form, Input, message } from 'antd'; +import { Button, Checkbox, Form, Input, message, Modal } from 'antd'; import { observer } from 'mobx-react-lite'; -import { Navigate,useNavigate } from 'react-router-dom'; +import { Navigate, useNavigate } from 'react-router-dom'; import UserLoginStore from '@/app/stores/user-login-store'; import UserApi from '@api/user/user.api'; import FRONTEND_ROUTES from '@constants/frontend-routes.constants'; -import useMobx from '@stores/root-store'; +import useMobx, { useModalContext } from '@stores/root-store'; const LoginPage = () => { const { userLoginStore } = useMobx(); + + const { modalStore: { setModal, modalsState: { forgotPassword } } } = useModalContext(); + const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); + const handleForgotPassword = async (values: { email: string }) => { + try { + await UserApi.forgotPassword({ email: values.email }); + message.success('Лист для скидання пароля відправлено на вашу пошту'); + setModal('forgotPassword', undefined, false); + } catch { + message.error('Помилка при відправці запиту'); + } + }; + const handleFinish = async (values: { login: string; password: string }) => { try { setIsLoading(true); - const response = await UserApi.adminLogin(values); - - userLoginStore.setUserLoginResponce( - response, - userLoginStore.refreshToken, - ); - + userLoginStore.setUserLoginResponce(response, userLoginStore.refreshToken); navigate(FRONTEND_ROUTES.ADMIN.BASE); } catch { message.error('Невірний логін або пароль'); @@ -44,66 +51,48 @@ const LoginPage = () => {

Вхід

Введіть свої дані для входу

-
- + + - +
- - - - Запам’ятати мене - + Запам’ятати мене
- + -
- Немає облікового запису? - -
- -
- - - - або продовжити через - - - -
- - - + + ); diff --git a/src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.component.tsx b/src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.component.tsx new file mode 100644 index 0000000..b08ab0f --- /dev/null +++ b/src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.component.tsx @@ -0,0 +1,84 @@ +import './ResetPasswordPage.styles.scss'; + +import { useState } from 'react'; +import { Button, Form, Input, message } from 'antd'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { API_ROUTES } from '@constants/api-routes.constants'; +import UserApi from '@api/user/user.api'; + +const ResetPasswordPage = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + + const token = searchParams.get('token'); + const email = searchParams.get('email'); + + const handleFinish = async (values: { password: string; confirm: string }) => { + if (values.password !== values.confirm) { + message.error('Паролі не співпадають'); + return; + } + if (!token || !email) { + message.error('Некоректне посилання для відновлення пароля'); + return; + } + try { + setIsLoading(true); + + await UserApi.resetPassword({ + email, + token, + newPassword: values.password + }); + + message.success('Пароль успішно змінено!'); + navigate(API_ROUTES.ADMIN_AUTHORIZATION.LOGIN); + } catch { + message.error('Помилка при зміні пароля. Можливо, посилання застаріло.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

Створення нового пароля

+

Введіть новий пароль для вашого облікового запису

+ +
+ + + + + + + + + +
+
+
+ ); +}; + +export default ResetPasswordPage; \ No newline at end of file diff --git a/src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.styles.scss b/src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.styles.scss new file mode 100644 index 0000000..3e243ab --- /dev/null +++ b/src/features/AdminPage/ResetPasswordPage/ResetPasswordPage.styles.scss @@ -0,0 +1,144 @@ +.loginPage { + min-height: calc(100vh - 120px); + display: flex; + justify-content: center; + align-items: center; + padding: 72px 16px; + background-color: #fafafa; +} + +.loginCard { + width: 100%; + max-width: 500px; + padding: 48px 52px 40px; + border-radius: 40px; + background-color: #fff; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16); + + .loginTitle { + margin-bottom: 8px; + text-align: center; + font-size: 24px; + font-weight: 700; + } +} + +.loginSubtitle { + margin-bottom: 32px; + text-align: center; + color: #6f6f6f; +} + +.loginOptions { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: -8px; + margin-bottom: 24px; + + .ant-btn-link { + padding: 0; + color: #9f211c; + font-weight: 600; + + &[disabled] { + color: #9f211c; + opacity: 0.7; + } + } +} + +.loginSubmitBtn { + height: 56px; + border: none; + border-radius: 8px; + background-color: #b71c1c; + color: #fff; + font-weight: 700; + + &:hover, + &:focus { + background-color: #c9322a; + color: #fff; + } +} + +.loginRegister { + margin-top: 12px; + text-align: center; + color: #777; + + .ant-btn-link { + padding: 0 0 0 4px; + color: #9f211c; + font-weight: 700; + } +} + +.loginDivider { + display: flex; + align-items: center; + gap: 12px; + margin: 28px 0 20px; + color: #999; + font-size: 14px; +} + +.dividerLine { + flex: 1; + height: 1px; + background-color: #e0e0e0; +} + +.dividerText { + flex: 0 0 auto; + height: auto; + background-color: transparent; + text-align: center; + line-height: 18px; +} + +.googleLoginBtn { + display: block; + min-width: 120px; + height: 44px; + margin: 0 auto; + border: none; + border-radius: 8px; + background-color: #f4f4f4; + font-weight: 600; +} + +@media (max-width: 768px) { + .loginPage { + align-items: flex-start; + padding-top: 64px; + } + + .loginCard { + max-width: 440px; + padding: 40px 36px; + border-radius: 36px; + } +} + +@media (max-width: 480px) { + .loginPage { + padding: 32px 16px; + background-color: #fff; + } + + .loginCard { + padding: 0; + border-radius: 0; + box-shadow: none; + } + + .loginOptions { + font-size: 12px; + } + + .loginSubmitBtn { + height: 52px; + } +} \ No newline at end of file diff --git a/src/models/user/user.model.ts b/src/models/user/user.model.ts index bdaedd1..5e09aed 100644 --- a/src/models/user/user.model.ts +++ b/src/models/user/user.model.ts @@ -35,3 +35,12 @@ export enum UserRole { Administrator, Moderator, } +export interface ResetPasswordRequest { + email: string; + token: string; + newPassword: string; +} +export interface ForgotPasswordRequest { + email: string; +} +