Skip to content

Commit

Permalink
Migrate reset password endpoint from REST to GraphQL (#13)
Browse files Browse the repository at this point in the history
In backend and frontend
  • Loading branch information
e-wai authored May 30, 2021
1 parent cd08a04 commit 01e8a7f
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 14 deletions.
4 changes: 4 additions & 0 deletions backend/python/app/graphql/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import os

from flask import current_app
from flask_graphql import GraphQLView

from ..models import db
from ..services.implementations.entity_service import EntityService
from ..services.implementations.user_service import UserService
from .schema import schema
from .service import services

Expand All @@ -16,3 +19,4 @@ def init_app(app):
)

services["entity"] = EntityService(current_app.logger)
services["user"] = UserService(current_app.logger)
39 changes: 38 additions & 1 deletion backend/python/app/graphql/mutations/auth_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,41 @@
refresh: String!
logout(userId: ID!): ID
resetPassword(email: String!): Boolean!
"""
"""

import os

import graphene
from flask import Blueprint, current_app, jsonify, request

from ...services.implementations.auth_service import AuthService
from ...services.implementations.email_service import EmailService
from ..service import services

email_service = EmailService(
current_app.logger,
{
"refresh_token": os.getenv("EMAIL_REFRESH_TOKEN"),
"token_uri": "https://oauth2.googleapis.com/token",
"client_id": os.getenv("EMAIL_CLIENT_ID"),
"client_secret": os.getenv("EMAIL_CLIENT_SECRET"),
},
"planetread@uwblueprint.org", # must replace
"Planet Read", # must replace)
)
auth_service = AuthService(current_app.logger, services["user"], email_service)


class ResetPassword(graphene.Mutation):
class Arguments:
email = graphene.String(required=True)

ok = graphene.Boolean()

def mutate(root, info, email):
try:
auth_service.reset_password(email)
return ResetPassword(ok=True)
except Exception as e:
error_message = getattr(e, "message", None)
raise Exception(error_message if error_message else str(e))
5 changes: 4 additions & 1 deletion backend/python/app/graphql/schema.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import graphene

from .mutations.auth_mutation import ResetPassword
from .mutations.entity_mutation import CreateEntity
from .mutations.user_mutation import CreateUser
from .queries.entity_query import resolve_entities
from .queries.user_query import resolve_user_by_email, resolve_user_by_id, resolve_users
from .queries.user_query import (resolve_user_by_email, resolve_user_by_id,
resolve_users)
from .types.entity_type import EntityResponseDTO
from .types.user_type import UserDTO


class Mutation(graphene.ObjectType):
create_entity = CreateEntity.Field()
create_user = CreateUser.Field()
reset_password = ResetPassword.Field()


class Query(graphene.ObjectType):
Expand Down
6 changes: 2 additions & 4 deletions backend/python/app/rest/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

from flask import Blueprint, current_app, jsonify, request

from ..middlewares.auth import (
require_authorization_by_email,
require_authorization_by_user_id,
)
from ..middlewares.auth import (require_authorization_by_email,
require_authorization_by_user_id)
from ..resources.create_user_dto import CreateUserDTO
from ..services.implementations.auth_service import AuthService
from ..services.implementations.email_service import EmailService
Expand Down
41 changes: 34 additions & 7 deletions frontend/src/components/auth/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import React, { useContext } from "react";
import authAPIClient from "../../APIClients/AuthAPIClient";
import { gql, useMutation } from "@apollo/client";
import AuthContext from "../../contexts/AuthContext";

interface ResetPasswordProps {
email?: string;
}

const RESET_PASSWORD = gql`
mutation ResetPassword($email: String!) {
resetPassword(email: $email) {
ok
}
}
`;

const ResetPassword = ({ email }: ResetPasswordProps) => {
const { authenticatedUser } = useContext(AuthContext);

Expand All @@ -14,17 +22,36 @@ const ResetPassword = ({ email }: ResetPasswordProps) => {
return re.test(String(emailString).toLowerCase());
};

type ResetPassword = { ok: boolean };
const [resetPassword] = useMutation<{ resetPassword: ResetPassword }>(
RESET_PASSWORD,
);

const handleErrorOnReset = (errorMessage: string) => {
alert(errorMessage);
};

const handleSuccessOnReset = (successMessage: string) => {
alert(successMessage);
};

const onResetPasswordClick = async () => {
if (authenticatedUser == null && !isRealEmailAvailable(email!)) {
alert("invalid email");
alert("Invalid email");
return;
}

const resetEmail = authenticatedUser?.email ?? email;
if (await authAPIClient.resetPassword(resetEmail)) {
alert(`Reset email sent to ${resetEmail}`);
} else {
alert(`Unsuccessful attempt to send reset email.
Check that email is correct.`);
try {
const result = await resetPassword({ variables: { email: resetEmail } });

if (result.data?.resetPassword.ok) {
handleSuccessOnReset(`Reset email sent to ${resetEmail}`);
} else {
handleErrorOnReset("Reset password failed.");
}
} catch (err) {
handleErrorOnReset(err ?? "Error occurred, please try again.");
}
};

Expand Down
74 changes: 73 additions & 1 deletion frontend/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,85 @@
import axios from "axios";
import jwt from "jsonwebtoken";
import React from "react";
import ReactDOM from "react-dom";
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";

import AUTHENTICATED_USER_KEY from "./constants/AuthConstants";
import {
getLocalStorageObjProperty,
setLocalStorageObjProperty,
} from "./utils/LocalStorageUtils";

import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

const REFRESH_MUTATION = `
mutation Index_Refresh {
refresh
}
`;

const link = createHttpLink({
uri: `${process.env.REACT_APP_BACKEND_URL}/graphql`,
credentials: "include",
});

const authLink = setContext(async (_, { headers }) => {
// get the authentication token from local storage if it exists
let token: string = getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
);

if (token) {
const decodedToken: any = jwt.decode(token);

// refresh if decodedToken has expired
if (
decodedToken &&
decodedToken.exp > Math.round(new Date().getTime() / 1000)
) {
const { data } = await axios.post(
`${process.env.REACT_APP_BACKEND_URL}/graphql`,
{ query: REFRESH_MUTATION },
{ withCredentials: true },
);

const accessToken: string = data.data.refresh;
setLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
accessToken,
);
token = accessToken;
}
}
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
});

const apolloClient = new ApolloClient({
link: authLink.concat(link),
cache: new InMemoryCache(),
});

ReactDOM.render(
<React.StrictMode>
<App />
<ApolloProvider client={apolloClient}>
<App />
</ApolloProvider>
</React.StrictMode>,
document.getElementById("root"),
);
Expand Down

0 comments on commit 01e8a7f

Please sign in to comment.