diff --git a/src/app/api/user/user.api.ts b/src/app/api/user/user.api.ts index 3dc3d7a..a16b2ad 100644 --- a/src/app/api/user/user.api.ts +++ b/src/app/api/user/user.api.ts @@ -1,11 +1,15 @@ import Agent from '@api/agent.api'; -import ChangePasswordDto from '@models/user/user.model'; +import { + ResetPasswordRequest, + ForgotPasswordRequest, + ChangePasswordDto, + RefreshTokenRequest, + RefreshTokenResponce, + UserLoginRequest, + UserLoginResponce, +} from '@models/user/user.model'; import { API_ROUTES } from '@/app/common/constants/api-routes.constants'; -import { - RefreshTokenRequest, RefreshTokenResponce, - UserLoginRequest, UserLoginResponce, -} from '@/models/user/user.model'; const UserApi = { login: (loginParams: UserLoginRequest) => @@ -43,7 +47,20 @@ const UserApi = { 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, + ), + changePassword: (data: ChangePasswordDto) => Agent.post(API_ROUTES.ADMIN_AUTHORIZATION.CHANGE_PASSWORD, data), }; + export default UserApi; \ No newline at end of file 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 7b87244..e6a1149 100644 --- a/src/app/common/constants/api-routes.constants.ts +++ b/src/app/common/constants/api-routes.constants.ts @@ -224,6 +224,8 @@ export const API_ROUTES = { REFRESH_TOKEN: "auth/refresh-token", LOGOUT: "auth/logout", CHANGE_PASSWORD: "auth/change-password", + RESET_PASSWORD: "auth/reset-password", + FORGOT_PASSWORD: "auth/forgot-password", }, COMMENTS: { GET_BY_STREETCODE_ID: "comment/getByStreetcodeId", diff --git a/src/app/common/constants/frontend-routes.constants.ts b/src/app/common/constants/frontend-routes.constants.ts index 09c9f8b..8dd292c 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 a109a63..aa06e3d 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 SettingsPage from "@/features/AdminPage/SettingsPage/SettingsPage.component"; import StreetcodeCatalog from "@/features/StreetcodeCatalogPage/StreetcodeCatalog.component"; @@ -39,6 +40,7 @@ const router = createBrowserRouter( } /> } /> } /> + } /> } /> } /> @@ -60,4 +62,4 @@ const router = createBrowserRouter( ), ); -export default router; +export default router; \ No newline at end of file diff --git a/src/app/stores/modal-store.ts b/src/app/stores/modal-store.ts index 49f6e19..cfdd61d 100644 --- a/src/app/stores/modal-store.ts +++ b/src/app/stores/modal-store.ts @@ -45,6 +45,7 @@ interface ModalList { adminFacts: ModalState; adminChronology: ModalState; statistics: ModalState; + forgotPassword: ModalState; editImage: ModalState; deleteImage: ModalState; deleteImageTemplates: ModalState; @@ -71,6 +72,7 @@ export default class ModalStore { adminFacts: { ...DefaultModalState }, adminChronology: { ...DefaultModalState }, statistics: { ...DefaultModalState }, + forgotPassword: { ...DefaultModalState }, editImage: { ...DefaultModalState }, deleteImage: { ...DefaultModalState }, deleteImageTemplates: { ...DefaultModalState }, diff --git a/src/features/AdminPage/LoginPage/LoginPage.component.tsx b/src/features/AdminPage/LoginPage/LoginPage.component.tsx index 47eb8fc..c5f9b58 100644 --- a/src/features/AdminPage/LoginPage/LoginPage.component.tsx +++ b/src/features/AdminPage/LoginPage/LoginPage.component.tsx @@ -2,31 +2,38 @@ import './LoginPage.styles.scss'; import { GoogleLogin } from '@react-oauth/google'; 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 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('Невірний логін або пароль'); @@ -45,59 +52,29 @@ const LoginPage = () => {

Вхід

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

-
- + + - +
- - - - Запам’ятати мене - + Запам’ятати мене
- -
- Немає облікового запису? - -
-
- - - або продовжити через - - + або продовжити через
@@ -122,6 +99,27 @@ const LoginPage = () => { }} /> + + setModal('forgotPassword', undefined, false)} + footer={null} + className="forgotPasswordModal" + > +
+ + + + +
+
); 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 1f46b11..d5092ce 100644 --- a/src/models/user/user.model.ts +++ b/src/models/user/user.model.ts @@ -41,3 +41,12 @@ export interface ChangePasswordDto { newPassword: string; confirmNewPassword: string; } +export interface ResetPasswordRequest { + email: string; + token: string; + newPassword: string; +} +export interface ForgotPasswordRequest { + email: string; +} +