diff --git a/backend/app/rest/auth_routes.py b/backend/app/rest/auth_routes.py index 22037031..7fe0dc85 100644 --- a/backend/app/rest/auth_routes.py +++ b/backend/app/rest/auth_routes.py @@ -4,7 +4,11 @@ InvalidPasswordException, TooManyLoginAttemptsException, ) -from ..utilities.exceptions.auth_exceptions import EmailAlreadyInUseException +from ..utilities.exceptions.auth_exceptions import ( + EmailAlreadyInUseException, + UserNotFoundException, + UserNotActiveException, +) from flask import Blueprint, current_app, jsonify, request @@ -235,7 +239,6 @@ def logout(user_id): @blueprint.route( "/resetPassword/", methods=["POST"], strict_slashes=False ) -@require_authorization_by_email("email") def reset_password(email): """ Triggers password reset for user with specified email (reset link will be emailed) @@ -243,6 +246,12 @@ def reset_password(email): try: auth_service.reset_password(email) return "", 204 + except UserNotFoundException as e: + error_message = getattr(e, "message", None) + return jsonify({"error": (error_message if error_message else str(e))}), 404 + except UserNotActiveException as e: + error_message = getattr(e, "message", None) + return jsonify({"error": (error_message if error_message else str(e))}), 403 except Exception as e: error_message = getattr(e, "message", None) return jsonify({"error": (error_message if error_message else str(e))}), 500 diff --git a/backend/app/services/implementations/auth_service.py b/backend/app/services/implementations/auth_service.py index a510e733..7539553b 100644 --- a/backend/app/services/implementations/auth_service.py +++ b/backend/app/services/implementations/auth_service.py @@ -4,6 +4,10 @@ InvalidPasswordException, TooManyLoginAttemptsException, ) +from ...utilities.exceptions.auth_exceptions import ( + UserNotActiveException, + UserNotFoundException, +) from ..interfaces.auth_service import IAuthService from ...resources.auth_dto import AuthDTO from ...resources.create_user_dto import CreateUserDTO @@ -127,6 +131,11 @@ def reset_password(self, email): raise Exception(error_message) try: + # verify the user exists and is Active + user = self.user_service.get_user_by_email(email) + if user.user_status != "Active": + raise UserNotActiveException + reset_link = firebase_admin.auth.generate_password_reset_link(email) email_body = """ Hello, @@ -140,6 +149,22 @@ def reset_password(self, email): reset_link=reset_link ) self.email_service.send_email(email, "Your Password Reset Link", email_body) + except UserNotFoundException as e: + reason = getattr(e, "message", None) + self.logger.error( + "Failed to send password reset link for {email}. Reason = {reason}".format( + email=email, reason=(reason if reason else str(e)) + ) + ) + raise e + except UserNotActiveException as e: + reason = getattr(e, "message", None) + self.logger.error( + "Failed to send password reset link for {email}. Reason = {reason}".format( + email=email, reason=(reason if reason else str(e)) + ) + ) + raise e except Exception as e: reason = getattr(e, "message", None) self.logger.error( diff --git a/backend/app/services/implementations/user_service.py b/backend/app/services/implementations/user_service.py index c7273aad..df262121 100644 --- a/backend/app/services/implementations/user_service.py +++ b/backend/app/services/implementations/user_service.py @@ -6,6 +6,7 @@ from ...resources.user_dto import UserDTO from ...utilities.exceptions.auth_exceptions import ( UserNotInvitedException, + UserNotFoundException, EmailAlreadyInUseException, ) from ...utilities.exceptions.duplicate_entity_exceptions import DuplicateUserException @@ -53,11 +54,7 @@ def get_user_by_email(self, email): user = User.query.filter_by(auth_id=firebase_user.uid).first() if not user: - raise Exception( - "user with auth_id {auth_id} not found".format( - auth_id=firebase_user.uid - ) - ) + raise UserNotFoundException user_dict = UserService.__user_to_dict_and_remove_auth_id(user) user_dict["email"] = firebase_user.email @@ -70,7 +67,7 @@ def get_user_by_email(self, email): reason=(reason if reason else str(e)) ) ) - raise e + raise UserNotFoundException def get_user_role_by_auth_id(self, auth_id): try: diff --git a/backend/app/utilities/exceptions/auth_exceptions.py b/backend/app/utilities/exceptions/auth_exceptions.py index f4069067..ef307c3d 100644 --- a/backend/app/utilities/exceptions/auth_exceptions.py +++ b/backend/app/utilities/exceptions/auth_exceptions.py @@ -8,6 +8,26 @@ def __init__(self): super().__init__(self.message) +class UserNotFoundException(Exception): + """ + Raised when a user is not found in the database by email + """ + + def __init__(self): + self.message = "This email address does not exist." + super().__init__(self.message) + + +class UserNotActiveException(Exception): + """ + Raised when a user does not have a user status of Active + """ + + def __init__(self): + self.message = "This email address is not currently active." + super().__init__(self.message) + + class EmailAlreadyInUseException(Exception): """ Raised when a user attempts to register with an email of a previously activated user diff --git a/frontend/src/APIClients/AuthAPIClient.ts b/frontend/src/APIClients/AuthAPIClient.ts index 11a68e5f..d036a3e5 100644 --- a/frontend/src/APIClients/AuthAPIClient.ts +++ b/frontend/src/APIClients/AuthAPIClient.ts @@ -123,7 +123,9 @@ const register = async ( } }; -const resetPassword = async (email: string | undefined): Promise => { +const resetPassword = async ( + email: string, +): Promise => { const bearerToken = `Bearer ${getLocalStorageObjProperty( AUTHENTICATED_USER_KEY, "accessToken", @@ -136,7 +138,25 @@ const resetPassword = async (email: string | undefined): Promise => { ); return true; } catch (error) { - return false; + const axiosErr = error as AxiosError; + + if (axiosErr.response && axiosErr.response.status === 403) { + return { + errMessage: + axiosErr.response.data.error ?? + "This email address is not currently active.", + }; + } + if (axiosErr.response && axiosErr.response.status === 404) { + return { + errMessage: + axiosErr.response.data.error ?? "This email address does not exist.", + }; + } + return { + errMessage: + "Unable to send password reset to this email address. Please try again.", + }; } }; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7484a26a..bcbc1086 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,6 @@ import { ChakraProvider } from "@chakra-ui/react"; import LoginPage from "./components/pages/Auth/LoginPage"; import SignupPage from "./components/pages/Auth/SignupPage"; import PrivateRoute from "./components/auth/PrivateRoute"; -import Verification from "./components/auth/Verification"; import HomePage from "./components/pages/HomePage/HomePage"; import NotFound from "./components/pages/Errors/NotFound"; import * as Routes from "./constants/Routes"; @@ -22,6 +21,8 @@ import customTheme from "./theme"; import EmployeeDirectoryPage from "./components/pages/AdminControls/EmployeeDirectory"; import SignInLogsPage from "./components/pages/AdminControls/SignInLogs"; import TagsPage from "./components/pages/AdminControls/Tags"; +import VerificationPage from "./components/pages/Auth/VerificationPage"; +import ResetPasswordPage from "./components/pages/Auth/ResetPasswordPage"; const App = (): React.ReactElement => { const currentUser: AuthenticatedUser | null = getLocalStorageObj( @@ -40,10 +41,15 @@ const App = (): React.ReactElement => { + { - const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext); - - const onLogOutClick = async () => { - const success = await authAPIClient.logout(authenticatedUser?.id); - if (success) { - setAuthenticatedUser(null); - } - }; - - return ( - - ); -}; - -export default Logout; diff --git a/frontend/src/components/auth/RefreshCredentials.tsx b/frontend/src/components/auth/RefreshCredentials.tsx deleted file mode 100644 index abbc8ecb..00000000 --- a/frontend/src/components/auth/RefreshCredentials.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { useContext } from "react"; - -import authAPIClient from "../../APIClients/AuthAPIClient"; -import AuthContext from "../../contexts/AuthContext"; - -const RefreshCredentials = (): React.ReactElement => { - const { setAuthenticatedUser } = useContext(AuthContext); - - const onRefreshClick = async () => { - const success = await authAPIClient.refresh(); - if (!success) { - setAuthenticatedUser(null); - } - }; - - return ( - - ); -}; - -export default RefreshCredentials; diff --git a/frontend/src/components/auth/ResetPassword.tsx b/frontend/src/components/auth/ResetPassword.tsx deleted file mode 100644 index d5151de2..00000000 --- a/frontend/src/components/auth/ResetPassword.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { useContext } from "react"; -import authAPIClient from "../../APIClients/AuthAPIClient"; -import AuthContext from "../../contexts/AuthContext"; - -const ResetPassword = (): React.ReactElement => { - const { authenticatedUser } = useContext(AuthContext); - - const onResetPasswordClick = async () => { - await authAPIClient.resetPassword(authenticatedUser?.email); - }; - - return ( - - ); -}; - -export default ResetPassword; diff --git a/frontend/src/components/forms/Login.tsx b/frontend/src/components/forms/Login.tsx index 83835073..a40e3d8e 100644 --- a/frontend/src/components/forms/Login.tsx +++ b/frontend/src/components/forms/Login.tsx @@ -12,7 +12,7 @@ import { import { useHistory } from "react-router-dom"; import authAPIClient from "../../APIClients/AuthAPIClient"; import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; -import { SIGNUP_PAGE } from "../../constants/Routes"; +import { RESET_PASSWORD_PAGE, SIGNUP_PAGE } from "../../constants/Routes"; import AuthContext from "../../contexts/AuthContext"; import { isAuthErrorResponse, isErrorResponse } from "../../helper/error"; import UserAPIClient from "../../APIClients/UserAPIClient"; @@ -148,10 +148,6 @@ const Login = ({ } }; - const onSignUpClick = () => { - history.push(SIGNUP_PAGE); - }; - if (toggle) { return ( @@ -202,15 +198,11 @@ const Login = ({ ) : ( + )} + + + + + Remembered your password? + + history.push(LOGIN_PAGE)} + > + Log In Now + + + + + ) : ( + <> + + Success! + + + + + {`A password reset email has been sent to ${email}. Click the link in the email and after completion, + you'll be able to sign in with your new password. If you did not receive an email, please try again.`} + + + + + + + + )} + + + + + {/* Background */} + + + ); +}; + +export default ResetPasswordPage; diff --git a/frontend/src/components/auth/Verification.tsx b/frontend/src/components/pages/Auth/VerificationPage.tsx similarity index 85% rename from frontend/src/components/auth/Verification.tsx rename to frontend/src/components/pages/Auth/VerificationPage.tsx index 5f4c0ff5..14222ca6 100644 --- a/frontend/src/components/auth/Verification.tsx +++ b/frontend/src/components/pages/Auth/VerificationPage.tsx @@ -1,14 +1,14 @@ import React, { useState, useContext } from "react"; import { useHistory } from "react-router-dom"; import { Box, Button, Flex, Spinner, Text } from "@chakra-ui/react"; -import authAPIClient from "../../APIClients/AuthAPIClient"; -import CreateToast from "../common/Toasts"; -import AuthContext from "../../contexts/AuthContext"; -import { HOME_PAGE } from "../../constants/Routes"; -import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; -import { AuthenticatedUser } from "../../types/AuthTypes"; +import authAPIClient from "../../../APIClients/AuthAPIClient"; +import CreateToast from "../../common/Toasts"; +import AuthContext from "../../../contexts/AuthContext"; +import { HOME_PAGE } from "../../../constants/Routes"; +import AUTHENTICATED_USER_KEY from "../../../constants/AuthConstants"; +import { AuthenticatedUser } from "../../../types/AuthTypes"; -const Verification = (): React.ReactElement => { +const VerificationPage = (): React.ReactElement => { const newToast = CreateToast(); const history = useHistory(); const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext); @@ -101,4 +101,4 @@ const Verification = (): React.ReactElement => { ); }; -export default Verification; +export default VerificationPage; diff --git a/frontend/src/constants/Routes.ts b/frontend/src/constants/Routes.ts index d1c2bfda..95908838 100644 --- a/frontend/src/constants/Routes.ts +++ b/frontend/src/constants/Routes.ts @@ -4,6 +4,8 @@ export const LOGIN_PAGE = "/login"; export const SIGNUP_PAGE = "/signup"; +export const RESET_PASSWORD_PAGE = "/reset-password"; + export const VERIFICATION_PAGE = "/verification"; export const RESIDENT_DIRECTORY_PAGE = "/resident-directory";