Skip to content

Commit

Permalink
Password reset (#230)
Browse files Browse the repository at this point in the history
* finish password reset integration of frontend with existing backend

* linterrr
  • Loading branch information
Connor Bechthold authored Feb 4, 2024
1 parent 64f7bb9 commit 354a64c
Show file tree
Hide file tree
Showing 14 changed files with 316 additions and 124 deletions.
13 changes: 11 additions & 2 deletions backend/app/rest/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -235,14 +239,19 @@ def logout(user_id):
@blueprint.route(
"/resetPassword/<string:email>", 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)
"""
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
Expand Down
25 changes: 25 additions & 0 deletions backend/app/services/implementations/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down
9 changes: 3 additions & 6 deletions backend/app/services/implementations/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions backend/app/utilities/exceptions/auth_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/APIClients/AuthAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ const register = async (
}
};

const resetPassword = async (email: string | undefined): Promise<boolean> => {
const resetPassword = async (
email: string,
): Promise<boolean | ErrorResponse> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
Expand All @@ -136,7 +138,25 @@ const resetPassword = async (email: string | undefined): Promise<boolean> => {
);
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.",
};
}
};

Expand Down
10 changes: 8 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<AuthenticatedUser>(
Expand All @@ -40,10 +41,15 @@ const App = (): React.ReactElement => {
<Switch>
<Route exact path={Routes.LOGIN_PAGE} component={LoginPage} />
<Route exact path={Routes.SIGNUP_PAGE} component={SignupPage} />
<Route
exact
path={Routes.RESET_PASSWORD_PAGE}
component={ResetPasswordPage}
/>
<PrivateRoute
exact
path={Routes.VERIFICATION_PAGE}
component={Verification}
component={VerificationPage}
/>
<PrivateRoute exact path={Routes.HOME_PAGE} component={HomePage} />
<PrivateRoute
Expand Down
23 changes: 0 additions & 23 deletions frontend/src/components/auth/Logout.tsx

This file was deleted.

23 changes: 0 additions & 23 deletions frontend/src/components/auth/RefreshCredentials.tsx

This file was deleted.

23 changes: 0 additions & 23 deletions frontend/src/components/auth/ResetPassword.tsx

This file was deleted.

39 changes: 21 additions & 18 deletions frontend/src/components/forms/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -148,10 +148,6 @@ const Login = ({
}
};

const onSignUpClick = () => {
history.push(SIGNUP_PAGE);
};

if (toggle) {
return (
<Flex h="100vh">
Expand Down Expand Up @@ -202,15 +198,11 @@ const Login = ({
) : (
<Button
variant="login"
_hover={
email && password
? {
background: "teal.500",
transition:
"transition: background-color 0.5s ease !important",
}
: {}
}
_hover={{
background: "teal.500",
transition:
"transition: background-color 0.5s ease !important",
}}
onClick={onLoginClick}
>
Log In
Expand All @@ -219,14 +211,25 @@ const Login = ({
</Box>
<Box w="80%">
<Flex gap="10px">
<Text variant="loginSecondary" paddingRight="17px">
Not a member yet?
</Text>
<Text variant="loginTertiary" onClick={onSignUpClick}>
<Text variant="loginSecondary">Not a member yet?</Text>
<Text
variant="loginTertiary"
onClick={() => history.push(SIGNUP_PAGE)}
>
Sign Up Now
</Text>
</Flex>
</Box>
<Box w="80%">
<Flex gap="10px">
<Text
variant="loginTertiary"
onClick={() => history.push(RESET_PASSWORD_PAGE)}
>
Forgot your password?
</Text>
</Flex>
</Box>
</Flex>
</Box>
<Box flex="1" bg="teal.400">
Expand Down
27 changes: 10 additions & 17 deletions frontend/src/components/forms/Signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,6 @@ const Signup = ({
}
};

const onLogInClick = () => {
history.push(LOGIN_PAGE);
};

if (authenticatedUser) {
return <Redirect to={HOME_PAGE} />;
}
Expand Down Expand Up @@ -277,15 +273,11 @@ const Signup = ({
) : (
<Button
variant="login"
_hover={
email && password && firstName && lastName
? {
background: "teal.500",
transition:
"transition: background-color 0.5s ease !important",
}
: {}
}
_hover={{
background: "teal.500",
transition:
"transition: background-color 0.5s ease !important",
}}
onClick={onSignupClick}
>
Create Account
Expand All @@ -294,10 +286,11 @@ const Signup = ({
</Box>
<Box w="80%">
<Flex gap="10px">
<Text variant="loginSecondary" paddingRight="1.1vw">
Already have an account?
</Text>
<Text variant="loginTertiary" onClick={onLogInClick}>
<Text variant="loginSecondary">Already have an account?</Text>
<Text
variant="loginTertiary"
onClick={() => history.push(LOGIN_PAGE)}
>
Log In Now
</Text>
</Flex>
Expand Down
Loading

0 comments on commit 354a64c

Please sign in to comment.