From 93569786736c32a194826539d1509886dfee5eee Mon Sep 17 00:00:00 2001 From: Rosnovskyi Yaroslav Date: Thu, 27 Mar 2025 09:36:53 +0200 Subject: [PATCH 1/2] added email confirmation --- src/app/api/authentication/auth.api.ts | 10 ++++- .../common/constants/api-routes.constants.ts | 2 + .../services/auth-service/AuthService.ts | 20 ++++++--- src/app/router/Routes.tsx | 2 + .../ConfirmEmail/ConfirmEmail.component.tsx | 43 +++++++++++++++++++ .../ForgotPasswordReset.component.tsx | 39 ++++++++++++++++- src/features/Auth/Login/Login.component.tsx | 34 +++++++-------- .../RegistrationPage.component.tsx | 7 ++- src/models/user/user.model.ts | 12 ++++++ 9 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 src/features/Auth/ConfirmEmail/ConfirmEmail.component.tsx diff --git a/src/app/api/authentication/auth.api.ts b/src/app/api/authentication/auth.api.ts index 87083512f..bb624fbf1 100644 --- a/src/app/api/authentication/auth.api.ts +++ b/src/app/api/authentication/auth.api.ts @@ -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' @@ -38,5 +38,11 @@ const AuthApi = { instance.post(API_ROUTES.AUTH.REFRESH_TOKEN, tokenTokenPapams) .then((response) => response.data) ), + confirmEmail: (confirmEmailRequest: ConfirmEmail) => ( + instance.post(API_ROUTES.AUTH.CONFIRM_EMAIL, confirmEmailRequest) + ), + validateToken: (validateTokenRequest: ValidateToken) => ( + instance.post(API_ROUTES.AUTH.VALIDATE_TOKEN, validateTokenRequest) + ), }; export default AuthApi; diff --git a/src/app/common/constants/api-routes.constants.ts b/src/app/common/constants/api-routes.constants.ts index f97599cde..6f605ba59 100644 --- a/src/app/common/constants/api-routes.constants.ts +++ b/src/app/common/constants/api-routes.constants.ts @@ -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', diff --git a/src/app/common/services/auth-service/AuthService.ts b/src/app/common/services/auth-service/AuthService.ts index 11e190e51..33a3b22d9 100644 --- a/src/app/common/services/auth-service/AuthService.ts +++ b/src/app/common/services/auth-service/AuthService.ts @@ -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; @@ -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( @@ -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(); diff --git a/src/app/router/Routes.tsx b/src/app/router/Routes.tsx index c09e66736..71e5d15c2 100644 --- a/src/app/router/Routes.tsx +++ b/src/app/router/Routes.tsx @@ -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'; @@ -121,6 +122,7 @@ const router = createBrowserRouter(createRoutesFromElements( /> } /> } /> + } /> , )); diff --git a/src/features/Auth/ConfirmEmail/ConfirmEmail.component.tsx b/src/features/Auth/ConfirmEmail/ConfirmEmail.component.tsx new file mode 100644 index 000000000..e7447f80b --- /dev/null +++ b/src/features/Auth/ConfirmEmail/ConfirmEmail.component.tsx @@ -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 ; +}; + +export default ConfirmEmail; diff --git a/src/features/Auth/ForgotPassword/ForgotPasswordReset.component.tsx b/src/features/Auth/ForgotPassword/ForgotPasswordReset.component.tsx index fcb48b495..a033f7718 100644 --- a/src/features/Auth/ForgotPassword/ForgotPasswordReset.component.tsx +++ b/src/features/Auth/ForgotPassword/ForgotPasswordReset.component.tsx @@ -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'; @@ -14,6 +16,39 @@ const ForgotPasswordResetComponent: React.FC = () => { const location = useLocation(); const navigator = useNavigate(); const [isValid, setIsValid] = useState(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); @@ -47,7 +82,7 @@ const ForgotPasswordResetComponent: React.FC = () => {