Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/app/api/user/user.api.ts
Original file line number Diff line number Diff line change
@@ -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) =>
Expand Down Expand Up @@ -43,7 +47,20 @@ const UserApi = {
API_ROUTES.ADMIN_AUTHORIZATION.LOGOUT, {},
),

forgotPassword: (forgotParams: ForgotPasswordRequest) =>
Agent.post<void>(
API_ROUTES.ADMIN_AUTHORIZATION.FORGOT_PASSWORD,
forgotParams,
),

resetPassword: (resetParams: ResetPasswordRequest) =>
Agent.post<void>(
API_ROUTES.ADMIN_AUTHORIZATION.RESET_PASSWORD,
resetParams,
),

changePassword: (data: ChangePasswordDto) =>
Agent.post<void>(API_ROUTES.ADMIN_AUTHORIZATION.CHANGE_PASSWORD, data),
};

export default UserApi;
Original file line number Diff line number Diff line change
@@ -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 (
<Modal
title="Відновлення пароля"
open={forgotPassword.isOpen}
onCancel={() => setModal('forgotPassword')}
footer={null}
className="forgotPasswordModal"
>
<Form layout="vertical" onFinish={onFinish}>
<Form.Item
name="email"
label="Електронна адреса"
rules={[{ required: true, type: 'email', message: 'Введіть коректний email' }]}
>
<Input />
</Form.Item>
<Button type="primary" htmlType="submit" block>
Відправити
</Button>
</Form>
</Modal>
);
};

export default observer(ForgotPasswordModal);
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
2 changes: 2 additions & 0 deletions src/app/common/constants/api-routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/app/common/constants/frontend-routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion src/app/router/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -39,6 +40,7 @@ const router = createBrowserRouter(
<Route path="calendar" element={<CalendarPage />} />
<Route path="news" element={<AdminNewsPage />} />
<Route path="vacancies" element={<VacanciesPage />} />
<Route path="reset-password" element={<ResetPasswordPage />} />
<Route path="settings" element={<SettingsPage />} />

<Route path={FRONTEND_ROUTES.ADMIN.NEW_STREETCODE} element={<EditPge />} />
Expand All @@ -60,4 +62,4 @@ const router = createBrowserRouter(
),
);

export default router;
export default router;
2 changes: 2 additions & 0 deletions src/app/stores/modal-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface ModalList {
adminFacts: ModalState;
adminChronology: ModalState;
statistics: ModalState;
forgotPassword: ModalState;
editImage: ModalState;
deleteImage: ModalState;
deleteImageTemplates: ModalState;
Expand All @@ -71,6 +72,7 @@ export default class ModalStore {
adminFacts: { ...DefaultModalState },
adminChronology: { ...DefaultModalState },
statistics: { ...DefaultModalState },
forgotPassword: { ...DefaultModalState },
editImage: { ...DefaultModalState },
deleteImage: { ...DefaultModalState },
deleteImageTemplates: { ...DefaultModalState },
Expand Down
90 changes: 44 additions & 46 deletions src/features/AdminPage/LoginPage/LoginPage.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('Невірний логін або пароль');
Expand All @@ -45,59 +52,29 @@ const LoginPage = () => {
<h1 className="loginTitle">Вхід</h1>
<p className="loginSubtitle">Введіть свої дані для входу</p>

<Form
layout="vertical"
onFinish={handleFinish}
>
<Form.Item
label="Електронна адреса"
name="login"
rules={[{ required: true, message: 'Введіть логін' }]}
>
<Form layout="vertical" onFinish={handleFinish}>
<Form.Item label="Електронна адреса" name="login" rules={[{ required: true, message: 'Введіть логін' }]}>
<Input maxLength={256} />
</Form.Item>

<Form.Item
label="Пароль"
name="password"
rules={[{ required: true, message: 'Введіть пароль' }]}
>
<Form.Item label="Пароль" name="password" rules={[{ required: true, message: 'Введіть пароль' }]}>
<Input.Password />
</Form.Item>

<div className="loginOptions">
<Button type="link" disabled>
<Button type="link" onClick={() => setModal('forgotPassword', undefined, true)}>
Забули пароль?
</Button>

<Checkbox defaultChecked>
Запам’ятати мене
</Checkbox>
<Checkbox defaultChecked>Запам’ятати мене</Checkbox>
</div>

<Button
className="loginSubmitBtn"
htmlType="submit"
loading={isLoading}
block
>
<Button className="loginSubmitBtn" htmlType="submit" loading={isLoading} block>
Увійти
</Button>

<div className="loginRegister">
Немає облікового запису?
<Button type="link" disabled>
Зареєструватися
</Button>
</div>

<div className="loginDivider">
<span className="dividerLine" />

<span className="dividerText">
або продовжити через
</span>

<span className="dividerText">або продовжити через</span>
<span className="dividerLine" />
</div>

Expand All @@ -122,6 +99,27 @@ const LoginPage = () => {
}}
/>
</Form>

<Modal
title="Відновлення пароля"
open={forgotPassword.isOpen}
onCancel={() => setModal('forgotPassword', undefined, false)}
footer={null}
className="forgotPasswordModal"
>
<Form layout="vertical" onFinish={handleForgotPassword}>
<Form.Item
name="email"
label="Електронна адреса"
rules={[{ required: true, type: 'email', message: 'Введіть коректний email' }]}
>
<Input />
</Form.Item>
<Button type="primary" htmlType="submit" block>
Відправити
</Button>
</Form>
</Modal>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="loginPage">
<div className="loginCard">
<h1 className="loginTitle">Створення нового пароля</h1>
<p className="loginSubtitle">Введіть новий пароль для вашого облікового запису</p>

<Form
layout="vertical"
onFinish={handleFinish}
>
<Form.Item
label="Новий пароль"
name="password"
rules={[{ required: true, message: 'Введіть новий пароль' }]}
>
<Input.Password maxLength={64} />
</Form.Item>

<Form.Item
label="Підтвердження пароля"
name="confirm"
rules={[{ required: true, message: 'Підтвердьте пароль' }]}
>
<Input.Password maxLength={64} />
</Form.Item>

<Button
className="loginSubmitBtn"
htmlType="submit"
loading={isLoading}
block
>
Зберегти пароль
</Button>
</Form>
</div>
</div>
);
};

export default ResetPasswordPage;
Loading