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
10 changes: 8 additions & 2 deletions src/app/api/authentication/auth.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import axios from 'axios';

import { API_ROUTES } from '@/app/common/constants/api-routes.constants';
import {
GoogleLoginRequest,
ConfirmEmail,
RefreshTokenRequest,
RefreshTokenResponce,
UserLoginRequest,
UserLoginResponse,
UserRegisterRequest,
UserRegisterResponse,
UserRegisterResponse, ValidateToken,
} from '@/models/user/user.model';

const defaultBaseUrl = process.env.NODE_ENV === 'development'
Expand Down Expand Up @@ -38,5 +38,11 @@ const AuthApi = {
instance.post<RefreshTokenResponce>(API_ROUTES.AUTH.REFRESH_TOKEN, tokenTokenPapams)
.then((response) => response.data)
),
confirmEmail: (confirmEmailRequest: ConfirmEmail) => (
instance.post<boolean>(API_ROUTES.AUTH.CONFIRM_EMAIL, confirmEmailRequest)
),
validateToken: (validateTokenRequest: ValidateToken) => (
instance.post<boolean>(API_ROUTES.AUTH.VALIDATE_TOKEN, validateTokenRequest)
),
};
export default AuthApi;
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 @@ -212,6 +212,8 @@ export const API_ROUTES = {
LOGIN_GOOGLE: 'auth/googleLogin',
REFRESH_TOKEN: 'auth/refreshToken',
REGISTER: 'auth/register',
CONFIRM_EMAIL: 'auth/confirmEmail',
VALIDATE_TOKEN: 'auth/validateToken',
},
EMAIL: {
SEND: 'email/send',
Expand Down
20 changes: 15 additions & 5 deletions src/app/common/services/auth-service/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import UsersApi from '@api/users/users.api';
import { jwtDecode, JwtPayload } from 'jwt-decode';

import AuthApi from '@/app/api/authentication/auth.api';
import { RefreshTokenRequest, UserRegisterRequest, UserRole } from '@/models/user/user.model';
import { RefreshTokenRequest, UserRegisterRequest, UserRole, ValidateToken } from '@/models/user/user.model';

interface CustomJwtPayload extends JwtPayload {
role: string;
Expand Down Expand Up @@ -69,10 +69,11 @@ export default class AuthService {
username: string | null,
token: string | null,
) {
await UsersApi.updateForgotPassword({ password, confirmPassword, username, token }).catch((error) => {
console.error(error);
return Promise.reject(error);
});
await UsersApi.updateForgotPassword({ password, confirmPassword, username, token })
.catch((error) => {
console.error(error);
return Promise.reject(error);
});
}

public static async registerAsync(
Expand All @@ -86,6 +87,15 @@ export default class AuthService {
}
}

public static async ValidateTokenAsync(request: ValidateToken) {
try {
await AuthApi.validateToken(request);
} catch (error) {
console.error(error);
return Promise.reject(error);
}
}

public static refreshTokenAsync = async () => {
try {
const oldAccesstoken = this.getAccessToken();
Expand Down
2 changes: 2 additions & 0 deletions src/app/router/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom';
import FRONTEND_ROUTES from '@constants/frontend-routes.constants';
import ContextMainPage from '@features/AdminPage/ContextPage/ContextMainPage.component';
import ConfirmEmail from '@features/Auth/ConfirmEmail/ConfirmEmail.component';
import ForgotPassword from '@features/Auth/ForgotPassword/ForgotPassword.component';
import ForgotPasswordResetComponent from '@features/Auth/ForgotPassword/ForgotPasswordReset.component';
import Login from '@features/Auth/Login/Login.component';
Expand Down Expand Up @@ -121,6 +122,7 @@ const router = createBrowserRouter(createRoutesFromElements(
/>
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/forgot-password-reset" element={<ForgotPasswordResetComponent />} />
<Route path="/confirm-email" element={<ConfirmEmail />} />
</Route>,
));

Expand Down
43 changes: 43 additions & 0 deletions src/features/Auth/ConfirmEmail/ConfirmEmail.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import AuthApi from '@api/authentication/auth.api';
import Loader from '@components/Loader/Loader.component';
import FRONTEND_ROUTES from '@constants/frontend-routes.constants';
import { useAsync } from '@hooks/stateful/useAsync.hook';

import { message } from 'antd';

const ConfirmEmail = () => {
const location = useLocation();
const navigate = useNavigate();
const hasRun = useRef(false);

useAsync(async () => {
if (hasRun.current) return;
hasRun.current = true;

const searchParams = new URLSearchParams(location.search);

const token = searchParams.get('token');
const userName = searchParams.get('username');

if (!token || !userName) {
navigate(FRONTEND_ROUTES.AUTH.LOGIN);
return;
}

try {
await AuthApi.confirmEmail({ userName, token });
message.success('Ваш акаунт успішно створено. Тепер ви можете увійти в систему.');
} catch (error) {
message.error('Це посилання недійсне або протерміноване.'
+ ' Поверніться на сторінку входу і спробуйте ще раз.');
}

navigate(FRONTEND_ROUTES.AUTH.LOGIN);
}, []);

return <Loader />;
};

export default ConfirmEmail;
39 changes: 37 additions & 2 deletions src/features/Auth/ForgotPassword/ForgotPasswordReset.component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import './ForgotPasswordReset.styles.scss';

import { useState } from 'react';
import { useRef, useState } from 'react';
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import { LeftOutlined } from '@ant-design/icons';
import AuthService from '@app/common/services/auth-service/AuthService';
import Password from '@components/Auth/Password.component';
import FRONTEND_ROUTES from '@constants/frontend-routes.constants';
import { useAsync } from '@hooks/stateful/useAsync.hook';
import { ValidateToken } from '@models/user/user.model';

import { Button, Form, message } from 'antd';

Expand All @@ -14,6 +16,39 @@ const ForgotPasswordResetComponent: React.FC = () => {
const location = useLocation();
const navigator = useNavigate();
const [isValid, setIsValid] = useState<boolean>(true);
const [isOkResult, setIsOkResult] = useState(false);
const hasRun = useRef(false);

useAsync(async () => {
if (hasRun.current) return;
hasRun.current = true;

const searchParams = new URLSearchParams(location.search);

const token = searchParams.get('token');
const userName = searchParams.get('username');

if (!token || !userName) {
navigator(FRONTEND_ROUTES.AUTH.LOGIN);
return;
}

const request: ValidateToken = {
token,
purpose: 'ResetPassword',
userName,
tokenProvider: 'CustomResetPassword',
};

try {
await AuthService.ValidateTokenAsync(request);
setIsOkResult(true);
} catch (error) {
message.error('Це посилання недійсне або протерміноване. '
+ 'Поверніться на сторінку відновлення паролю і спробуйте ще раз.', 5);
setIsOkResult(false);
}
}, []);

const handleUpdatePassword = async () => {
const searchParams = new URLSearchParams(location.search);
Expand Down Expand Up @@ -47,7 +82,7 @@ const ForgotPasswordResetComponent: React.FC = () => {
<Password onPasswordValid={setIsValid} />
<Button
htmlType="submit"
disabled={!isValid}
disabled={!isValid && !isOkResult}
Comment thread
YaroslavRosnovskyi marked this conversation as resolved.
className="streetcode-custom-button forgotPasswordResetButton"
>
<p>Відновити пароль</p>
Expand Down
34 changes: 15 additions & 19 deletions src/features/Auth/Login/Login.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, { useRef, useState } from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import AuthService from '@app/common/services/auth-service/AuthService';
import { ERROR_MESSAGES, INVALID_LOGIN_ATTEMPT } from '@constants/error-messages.constants';
import { ERROR_MESSAGES } from '@constants/error-messages.constants';
import FRONTEND_ROUTES from '@constants/frontend-routes.constants';
import validateEmail from '@utils/userValidators/validateEmail';

Expand Down Expand Up @@ -45,24 +45,20 @@ const Login: React.FC = () => {

const handleLogin = async ({ login, password }: any) => {
if (isVerified) {
try {
const token = recaptchaRef?.current?.getValue();
await AuthService.loginAsync(login, password, token)
.then(() => {
message.success('Ви успішно увійшли в систему.');
navigate(location.state.previousUrl || FRONTEND_ROUTES.BASE);
})
.catch((ex) => {
if (ex.response?.data) {
Object.keys(ex.response.data.message).forEach((key) => {
message.error(`${ex.response.data[key].message}`);
});
}
});
recaptchaRef.current?.reset();
} catch (error) {
message.error(INVALID_LOGIN_ATTEMPT);
}
const token = recaptchaRef?.current?.getValue();
await AuthService.loginAsync(login, password, token)
.then(() => {
message.success('Ви успішно увійшли в систему.');
navigate(location.state.previousUrl || FRONTEND_ROUTES.BASE);
})
.catch((ex) => {
if (ex.response?.data) {
Object.keys(ex.response.data[0]).forEach((key, index) => {
message.error(`${ex.response.data[index].message}`);
});
}
});
recaptchaRef.current?.reset();
Comment thread
YaroslavRosnovskyi marked this conversation as resolved.
} else {
message.error(RECAPTCHA_CHECK);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import validateLength from '@utils/userValidators/validateLength';
import validatePatternNameSurname from '@utils/userValidators/validatePatternNameSurname';
import validateRequired from '@utils/userValidators/validateRequired';


import { Button, Form, Input, message } from 'antd';

const RegistrationPage: React.FC = () => {
Expand All @@ -29,7 +28,14 @@ const RegistrationPage: React.FC = () => {
const handleRegister = async (request: UserRegisterRequest) => {
try {
await AuthService.registerAsync(request)
.then(() => navigate(FRONTEND_ROUTES.AUTH.LOGIN))
.then(() => {
navigate(FRONTEND_ROUTES.AUTH.LOGIN);
message.success(
`Лист надіслано на ${request.email}.
Перевірте пошту та перейдіть за посиланням, щоб підтвердити електронну адресу.
Посилання дійсне протягом 15 хвилин.`,
);
})
.catch((ex) => {
Object.keys(ex.response.data.errors).forEach((key) => {
message.error(`${ex.response.data.errors[key]}`);
Expand Down
12 changes: 12 additions & 0 deletions src/models/user/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ export interface RefreshTokenResponce {
accessToken:string;
}

export interface ConfirmEmail {
userName: string;
token: string;
}

export interface ValidateToken {
token: string;
purpose: string;
userName: string;
tokenProvider: string;
}

export enum UserRole {
Admin,
User,
Expand Down