Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forgot password backend logic + frontend design #73

Merged
merged 11 commits into from
Nov 11, 2024
21 changes: 8 additions & 13 deletions backend/rest/authRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { CookieOptions, Router } from "express";
import { generate } from "generate-password";
import {
getAccessToken,
isAuthorizedByEmail,
isAuthorizedByUserId,
isAuthorizedByRole,
isFirstTimeInvitedUser,
Expand Down Expand Up @@ -144,18 +143,14 @@ authRouter.post(
);

/* Emails a password reset link to the user with the specified email */
authRouter.post(
"/resetPassword/:email",
isAuthorizedByEmail("email"),
async (req, res) => {
try {
await authService.resetPassword(req.params.email);
res.status(204).send();
} catch (error: unknown) {
res.status(500).json({ error: getErrorMessage(error) });
}
},
);
authRouter.post("/resetPassword/:email", async (req, res) => {
try {
await authService.resetPassword(req.params.email);
res.status(204).send();
} catch (error: unknown) {
res.status(500).json({ error: getErrorMessage(error) });
}
});

authRouter.post("/isUserVerified/:email", async (req, res) => {
try {
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/lexend-deca": "^5.1.0",
"@fontsource/roboto": "^5.0.13",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
Expand Down
12 changes: 2 additions & 10 deletions frontend/src/APIClients/AuthAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,9 @@ const signup = async (
}
};

const resetPassword = async (email: string | undefined): Promise<boolean> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
)}`;
const resetPassword = async (email: string): Promise<boolean> => {
try {
await baseAPIClient.post(
`/auth/resetPassword/${email}`,
{},
{ headers: { Authorization: bearerToken } },
);
await baseAPIClient.post(`/auth/resetPassword/${email}`, {});
return true;
} catch (error) {
return false;
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/APIClients/CourseAPIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import AUTHENTICATED_USER_KEY from "../constants/AuthConstants";
import { CourseUnit } from "../types/CourseTypes";
import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils";
import baseAPIClient from "./BaseAPIClient";

const getUnits = async (): Promise<CourseUnit[]> => {
const bearerToken = `Bearer ${getLocalStorageObjProperty(
AUTHENTICATED_USER_KEY,
"accessToken",
)}`;
try {
const { data } = await baseAPIClient.get("/course/", {
headers: { Authorization: bearerToken },
});
return data;
} catch (error) {
return [];
}
};

export default { getUnits };
25 changes: 19 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "bootstrap/dist/css/bootstrap.min.css";
import { CssBaseline } from "@mui/material";
import React, { useState, useReducer, useEffect } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Welcome from "./components/pages/Welcome";
Expand All @@ -18,16 +19,18 @@ import SampleContext, {
} from "./contexts/SampleContext";
import sampleContextReducer from "./reducers/SampleContextReducer";
import SampleContextDispatcherContext from "./contexts/SampleContextDispatcherContext";

import { AuthenticatedUser } from "./types/AuthTypes";
import authAPIClient from "./APIClients/AuthAPIClient";
import * as Routes from "./constants/Routes";
import ManageUserPage from "./components/pages/ManageUserPage";
import { SocketProvider } from "./contexts/SocketContext";

import ManageUserPage from "./components/pages/ManageUserPage";
import MakeHelpRequestPage from "./components/pages/MakeHelpRequestPage";
import ViewHelpRequestsPage from "./components/pages/ViewHelpRequestsPage";
import HelpRequestPage from "./components/pages/HelpRequestPage";
import CreatePasswordPage from "./components/pages/CreatePasswordPage";
import ForgotPasswordPage from "./components/auth/forgot_password/ForgotPasswordPage";
import CourseUnitsPage from "./components/pages/courses/CourseUnitsPage";

const App = (): React.ReactElement => {
const currentUser: AuthenticatedUser | null =
Expand All @@ -36,9 +39,6 @@ const App = (): React.ReactElement => {
const [authenticatedUser, setAuthenticatedUser] =
useState<AuthenticatedUser | null>(currentUser);

// Some sort of global state. Context API replaces redux.
// Split related states into different contexts as necessary.
// Split dispatcher and state into separate contexts as necessary.
const [sampleContext, dispatchSampleContextUpdate] = useReducer(
sampleContextReducer,
DEFAULT_SAMPLE_CONTEXT,
Expand All @@ -55,11 +55,12 @@ const App = (): React.ReactElement => {
}
}, HOUR_MS);

return () => clearInterval(interval); // This represents the unmount function, in which you need to clear your interval to prevent memory leaks.
return () => clearInterval(interval);
}, [currentUser]);

return (
<SampleContext.Provider value={sampleContext}>
<CssBaseline />
<SampleContextDispatcherContext.Provider
value={dispatchSampleContextUpdate}
>
Expand All @@ -72,6 +73,12 @@ const App = (): React.ReactElement => {
<Route exact path={Routes.WELCOME_PAGE} component={Welcome} />
<Route exact path={Routes.LOGIN_PAGE} component={Login} />
<Route exact path={Routes.SIGNUP_PAGE} component={Signup} />
<Route
exact
path={Routes.FORGOT_PASSWORD_PAGE}
component={ForgotPasswordPage}
/>

<PrivateRoute
exact
path={Routes.HOME_PAGE}
Expand Down Expand Up @@ -125,6 +132,12 @@ const App = (): React.ReactElement => {
component={HelpRequestPage}
allowedRoles={["Facilitator"]}
/>
<PrivateRoute
exact
path={Routes.COURSES_PAGE}
component={CourseUnitsPage}
allowedRoles={["Administrator", "Facilitator", "Learner"]}
/>
<Route exact path="*" component={NotFound} />
</Switch>
</Router>
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/auth/ResetPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ const ResetPassword = (): React.ReactElement => {
const { authenticatedUser } = useContext(AuthContext);

const onResetPasswordClick = async () => {
await authAPIClient.resetPassword(authenticatedUser?.email);
if (!authenticatedUser?.email) {
return;
}
await authAPIClient.resetPassword(authenticatedUser.email);
};

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import React, { useEffect, useState } from "react";
import { Container, Typography, useTheme, Snackbar, Box } from "@mui/material";
import SendIcon from "@mui/icons-material/Send";
import authAPIClient from "../../../APIClients/AuthAPIClient";

interface ForgotPasswordConfirmationProps {
email: string;
onBackToEmail: () => void;
}

const ForgotPasswordConfirmation: React.FC<ForgotPasswordConfirmationProps> = ({
email,
onBackToEmail,
}) => {
const theme = useTheme();
const [seconds, setSeconds] = useState(30);
const [canResend, setCanResend] = useState(false);
const [showSnackbar, setShowSnackbar] = useState(false);

useEffect(() => {
if (seconds > 0) {
const timer = setTimeout(() => setSeconds(seconds - 1), 1000);
return () => clearTimeout(timer);
}

setCanResend(true);
return undefined;
}, [seconds]);

const handleResendEmail = async () => {
if (canResend) {
setSeconds(30);
setCanResend(false);
try {
await authAPIClient.resetPassword(email);
setShowSnackbar(true);
setTimeout(() => setShowSnackbar(false), 5000);
} catch (error) {
/* eslint-disable-next-line no-console */
console.error("Error resending email:", error);
}
}
};

return (
<Container
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
width: "100vw",
backgroundColor: theme.palette.background?.default,
}}
>
<Container
sx={{
display: "flex",
width: "500px",
flexDirection: "column",
alignItems: "flex-start",
gap: "20px",
flexShrink: 0,
backgroundColor: theme.palette.background?.paper,
padding: theme.spacing(3),
borderRadius: theme.shape.borderRadius,
}}
>
<Container
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: theme.spacing(1),
alignSelf: "stretch",
marginLeft: theme.spacing(-3),
}}
>
<Typography
variant="h4"
gutterBottom
sx={{
color: "#000",
fontSize: "28px",
fontWeight: 600,
lineHeight: "120%",
marginBottom: 0,
}}
>
Email sent
</Typography>
<Typography
variant="body1"
sx={{
color: "#000",
fontSize: "16px",
fontWeight: 400,
lineHeight: "140%",
letterSpacing: "0.2px",
}}
>
Check your email ({email}) and open the link we sent to continue.
</Typography>
</Container>
<Typography
variant="bodySmall"
onClick={onBackToEmail}
sx={{
color: theme.palette.learner?.main,
fontSize: "12.5px",
fontWeight: 300,
lineHeight: "120%",
letterSpacing: "0.625px",
textTransform: "uppercase",
cursor: "pointer",
padding: 0,
}}
>
NOT YOUR EMAIL? Go back to change your email
</Typography>
<Typography
variant="bodySmall"
onClick={handleResendEmail}
sx={{
color: canResend
? theme.palette.learner?.main
: theme.palette.text?.disabled,
fontSize: theme.typography.bodySmall,
fontWeight: 300,
lineHeight: "120%",
letterSpacing: "0.625px",
textTransform: "uppercase",
cursor: canResend ? "pointer" : "default",
padding: 0,
}}
>
{canResend
? "Didn’t get the email? Send it again now."
: `Didn’t get the email? You can request a new one in ${seconds}s.`}
</Typography>
</Container>
<Snackbar
open={showSnackbar}
onClose={() => setShowSnackbar(false)}
anchorOrigin={{
vertical: "top",
horizontal: "center",
}}
autoHideDuration={5000}
sx={{
display: "inline-flex",
maxWidth: "500px",
width: "100%",
}}
ContentProps={{
sx: {
display: "inline-flex",
padding: "20px 32px",
alignItems: "center",
gap: "16px",
borderRadius: "8px",
backgroundColor: "#EDF2BD !important",
color: theme.palette.text?.primary,
fontSize: theme.typography.bodyMedium?.fontSize,
fontWeight: 400,
lineHeight: "1.5",
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.1)",
},
}}
>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
backgroundColor: "#EDF2BD",
padding: "20px 32px",
borderRadius: "var(--Radius-200, 8px)",
}}
>
<SendIcon
sx={{
color: "#6C7517",
marginRight: "16px",
}}
/>
<Typography
sx={{
color: "#6C7517",
}}
>
{`A new reset email has been sent.
Please check your inbox (and spam folder).`}
</Typography>
</Box>
</Snackbar>
</Container>
);
};

export default ForgotPasswordConfirmation;
Loading
Loading