From 13a7217bed49e97ffce67462237cc6e428b6d735 Mon Sep 17 00:00:00 2001 From: Julian Dominguez-Schatz Date: Tue, 3 Oct 2023 22:46:52 -0400 Subject: [PATCH] fix: admin verification issues (#594) * Remove dangerous `RESET_PASSWORD_CODE` mutation * Refactor mutation to query since there is no mutating occurring * Add new query to get the verification status of a user * Update frontend logic to match new backend endpoints * Refactor front-end to fix refresh bug * Refactor backend URL generation to pass the expected params * Fix logic error in forgot-password component * Fix failing tests * PR feedback: unused function * PR feedback: clarify logic --- backend/graphql/index.ts | 7 +- backend/graphql/resolvers/authResolvers.ts | 23 ++--- backend/graphql/resolvers/userResolvers.ts | 6 ++ backend/graphql/types/authType.ts | 6 +- backend/graphql/types/userType.ts | 9 ++ backend/middlewares/auth.ts | 18 ++++ .../services/implementations/authService.ts | 67 ++++++++++----- .../services/implementations/userService.ts | 6 ++ backend/services/interfaces/authService.ts | 10 +-- backend/testUtils/users.ts | 2 + backend/types/index.ts | 9 +- .../src/APIClients/mutations/AuthMutations.ts | 12 --- .../src/APIClients/queries/AuthQueries.ts | 7 ++ .../src/APIClients/queries/UserQueries.ts | 10 +++ .../src/APIClients/types/UserClientTypes.ts | 7 ++ .../auth/AdminSignupConfirmation.tsx | 86 +++++++------------ .../auth/email-action/EmailActionHandler.tsx | 31 +++---- .../email-action/ResetPasswordHandler.tsx | 56 ++++++------ .../auth/email-action/VerifyEmailHandler.tsx | 85 ++++++++++-------- .../auth/reset-password/ForgotPassword.tsx | 4 +- 20 files changed, 261 insertions(+), 200 deletions(-) create mode 100644 frontend/src/APIClients/queries/AuthQueries.ts diff --git a/backend/graphql/index.ts b/backend/graphql/index.ts index 8b1eec584..1234b73aa 100644 --- a/backend/graphql/index.ts +++ b/backend/graphql/index.ts @@ -3,7 +3,11 @@ import gql from "graphql-tag"; import { applyMiddleware } from "graphql-middleware"; import { merge } from "lodash"; -import { isAuthorizedByRole, isAuthorizedByUserId } from "../middlewares/auth"; +import { + isAuthorizedByRole, + isAuthorizedByUserId, + isAuthorizedForEveryone, +} from "../middlewares/auth"; import authResolvers from "./resolvers/authResolvers"; import authType from "./types/authType"; import schoolResolvers from "./resolvers/schoolResolvers"; @@ -61,6 +65,7 @@ const buildSchema = async () => { const graphQLMiddlewares = { Query: { + userVerificationStatus: isAuthorizedForEveryone(), tests: authorizedByAllRoles(), schoolByTeacherId: isAuthorizedByUserId("teacherId"), }, diff --git a/backend/graphql/resolvers/authResolvers.ts b/backend/graphql/resolvers/authResolvers.ts index 5e8a97a73..795ce456f 100644 --- a/backend/graphql/resolvers/authResolvers.ts +++ b/backend/graphql/resolvers/authResolvers.ts @@ -23,6 +23,15 @@ const cookieOptions: CookieOptions = { }; const authResolvers = { + Query: { + verifyPasswordResetCode: async ( + _parent: undefined, + { oobCode }: { oobCode: string }, + ): Promise => { + const email = await authService.verifyPasswordResetCode(oobCode); + return email; + }, + }, Mutation: { login: async ( _parent: undefined, @@ -104,13 +113,6 @@ const authResolvers = { await authService.resetPassword(email); return true; }, - resetPasswordCode: async ( - _parent: undefined, - { email }: { email: string }, - ): Promise => { - const oobCode = await authService.resetPasswordCode(email); - return oobCode; - }, verifyEmail: async ( _parent: undefined, { oobCode }: { oobCode: string }, @@ -118,13 +120,6 @@ const authResolvers = { const email = await authService.verifyEmail(oobCode); return email; }, - verifyPasswordReset: async ( - _parent: undefined, - { oobCode }: { oobCode: string }, - ): Promise => { - const email = await authService.verifyPasswordReset(oobCode); - return email; - }, confirmPasswordReset: async ( _parent: undefined, { newPassword, oobCode }: { newPassword: string; oobCode: string }, diff --git a/backend/graphql/resolvers/userResolvers.ts b/backend/graphql/resolvers/userResolvers.ts index f6a2d8075..f298a7146 100644 --- a/backend/graphql/resolvers/userResolvers.ts +++ b/backend/graphql/resolvers/userResolvers.ts @@ -13,6 +13,12 @@ const authService: IAuthService = new AuthService(userService, emailService); const userResolvers = { Query: { + userVerificationStatus: async ( + _parent: undefined, + { id }: { id: string }, + ): Promise => { + return userService.getUserById(id); + }, userByEmail: async ( _parent: undefined, { email }: { email: string }, diff --git a/backend/graphql/types/authType.ts b/backend/graphql/types/authType.ts index 0ef2e4474..511cb5b3c 100644 --- a/backend/graphql/types/authType.ts +++ b/backend/graphql/types/authType.ts @@ -30,15 +30,17 @@ const authType = gql` school: SchoolMetadataInput! } + extend type Query { + verifyPasswordResetCode(oobCode: String!): String! + } + extend type Mutation { login(email: String!, password: String!): AuthDTO! registerTeacher(user: RegisterTeacherDTO!): AuthDTO! refresh: String! logout(userId: ID!): ID resetPassword(email: String!): Boolean! - resetPasswordCode(email: String!): String! verifyEmail(oobCode: String!): String! - verifyPasswordReset(oobCode: String!): String! confirmPasswordReset(newPassword: String!, oobCode: String!): Boolean! sendEmailVerificationLink(email: String!): Boolean! } diff --git a/backend/graphql/types/userType.ts b/backend/graphql/types/userType.ts index 40c5d9372..5439e0d3d 100644 --- a/backend/graphql/types/userType.ts +++ b/backend/graphql/types/userType.ts @@ -15,6 +15,14 @@ const userType = gql` grades: [GradeEnum] currentlyTeachingJM: Boolean class: [String] + isVerified: Boolean! + } + + type UserVerificationStatusDTO { + id: ID! + email: String! + role: Role! + isVerified: Boolean! } type TeacherDTO { @@ -50,6 +58,7 @@ const userType = gql` } extend type Query { + userVerificationStatus(id: ID!): UserVerificationStatusDTO! userByEmail(email: String!): UserDTO! usersByRole(role: String!): [UserDTO!]! teachers: [TeacherDTO] diff --git a/backend/middlewares/auth.ts b/backend/middlewares/auth.ts index 8e54556d6..6ecc67d7f 100644 --- a/backend/middlewares/auth.ts +++ b/backend/middlewares/auth.ts @@ -29,6 +29,24 @@ export const getAccessToken = (req: Request): string | null => { return null; }; +/* No-op authorization check which always resolves */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const isAuthorizedForEveryone = + () => + async ( + resolve: ( + parent: any, + args: { [key: string]: any }, + context: ExpressContext, + info: GraphQLResolveInfo, + ) => any, + parent: any, + args: { [key: string]: any }, + context: ExpressContext, + info: GraphQLResolveInfo, + ) => + resolve(parent, args, context, info); + /* Determine if request is authorized based on accessToken validity and role of client */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ diff --git a/backend/services/implementations/authService.ts b/backend/services/implementations/authService.ts index 0233f2e7d..9c9d72745 100644 --- a/backend/services/implementations/authService.ts +++ b/backend/services/implementations/authService.ts @@ -129,6 +129,24 @@ class AuthService implements IAuthService { `; } + private getURLWithoutSearch(link: string): URL { + const url = new URL(link); + url.search = ""; + return url; + } + + private copyOobCodeToURL(link: URL, source: string, key: string): URL { + const sourceURL = new URL(source); + const oobCode = sourceURL.searchParams.get("oobCode"); + if (!oobCode) { + const errorMessage = `Failed to extract oobCode from link ${link}`; + Logger.error(errorMessage); + throw new Error(errorMessage); + } + link.searchParams.append(key, oobCode); + return link; + } + async resetPassword(email: string): Promise { if (!this.emailService) { const errorMessage = @@ -139,9 +157,14 @@ class AuthService implements IAuthService { try { const user = await this.userService.getUserByEmail(email); - const resetLink = await firebaseAdmin + const firebaseResetLink = await firebaseAdmin .auth() .generatePasswordResetLink(email); + const resetLink = this.copyOobCodeToURL( + this.getURLWithoutSearch(firebaseResetLink), + firebaseResetLink, + "resetPasswordOobCode", + ).toString(); const emailBody = this.getEmailTemplate( "password-reset-header.png", @@ -164,25 +187,6 @@ class AuthService implements IAuthService { } } - async resetPasswordCode(email: string): Promise { - let oobCode: string; - try { - const resetLink = await firebaseAdmin - .auth() - .generatePasswordResetLink(email); - const regex = /(?<=&oobCode=)(.*)(?=&apiKey=)/gm; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - [oobCode] = resetLink.match(regex)!; - } catch (error) { - Logger.error( - `Failed to generate password reset code for user with email ${email}`, - ); - throw error; - } - - return oobCode; - } - async sendEmailVerificationLink(email: string): Promise { if (!this.emailService) { const errorMessage = @@ -193,9 +197,28 @@ class AuthService implements IAuthService { try { const user = await this.userService.getUserByEmail(email); - const emailVerificationLink = await firebaseAdmin + const firebaseVerificationLink = await firebaseAdmin .auth() .generateEmailVerificationLink(email); + let resetLinkURL = this.copyOobCodeToURL( + this.getURLWithoutSearch(firebaseVerificationLink), + firebaseVerificationLink, + "verifyEmailOobCode", + ); + if (user.role === "Admin") { + const firebaseResetLink = await firebaseAdmin + .auth() + .generatePasswordResetLink(email); + + resetLinkURL = this.copyOobCodeToURL( + resetLinkURL, + firebaseResetLink, + "resetPasswordOobCode", + ); + } + resetLinkURL.searchParams.append("userId", user.id); + const emailVerificationLink = resetLinkURL.toString(); + const emailBody = this.getEmailTemplate( "email-header.png", user.firstName, @@ -303,7 +326,7 @@ class AuthService implements IAuthService { return res.email; } - async verifyPasswordReset(oobCode: string): Promise { + async verifyPasswordResetCode(oobCode: string): Promise { let res: ResetPasswordResponse; try { res = await FirebaseRestClient.verifyPasswordResetCode(oobCode); diff --git a/backend/services/implementations/userService.ts b/backend/services/implementations/userService.ts index 29fe42cad..ce307b574 100644 --- a/backend/services/implementations/userService.ts +++ b/backend/services/implementations/userService.ts @@ -51,6 +51,7 @@ class UserService implements IUserService { role: user.role, grades: user.grades, currentlyTeachingJM: user.currentlyTeachingJM, + isVerified: firebaseUser.emailVerified, }; } @@ -78,6 +79,7 @@ class UserService implements IUserService { role: user.role, grades: user.grades, currentlyTeachingJM: user.currentlyTeachingJM, + isVerified: firebaseUser.emailVerified, }; } @@ -143,6 +145,7 @@ class UserService implements IUserService { role: user.role, grades: user.grades, currentlyTeachingJM: user.currentlyTeachingJM, + isVerified: firebaseUser.emailVerified, }; }), ); @@ -204,6 +207,7 @@ class UserService implements IUserService { role: newUser.role, grades: newUser.grades, currentlyTeachingJM: newUser.currentlyTeachingJM, + isVerified: firebaseUser.emailVerified, }; } @@ -272,6 +276,7 @@ class UserService implements IUserService { role: user.role, grades: user.grades, currentlyTeachingJM: user.currentlyTeachingJM, + isVerified: updatedFirebaseUser.emailVerified, }; } @@ -362,6 +367,7 @@ class UserService implements IUserService { role: user.role, grades: user.grades, currentlyTeachingJM: user.currentlyTeachingJM, + isVerified: firebaseUser.emailVerified, }; }), ); diff --git a/backend/services/interfaces/authService.ts b/backend/services/interfaces/authService.ts index 7aeae02f5..228cb7312 100644 --- a/backend/services/interfaces/authService.ts +++ b/backend/services/interfaces/authService.ts @@ -34,14 +34,6 @@ interface IAuthService { */ resetPassword(email: string): Promise; - /** - * Generate a password reset code for the user with the given email - * @param email email of user requesting password reset - * @returns oobCode for password reset - * @throws Error if unable to generate code - */ - resetPasswordCode(email: string): Promise; - /** * Generate an email verification link for the user with the given email and send * the link to that email address @@ -94,7 +86,7 @@ interface IAuthService { * @param oobCode email action code sent to the user's email for resetting the password * @returns the user's email if the password reset code is valid, empty string otherwise */ - verifyPasswordReset(oobCode: string): Promise; + verifyPasswordResetCode(oobCode: string): Promise; /** * Apply a password reset change to the account of the user with the given oobCode diff --git a/backend/testUtils/users.ts b/backend/testUtils/users.ts index 56c81f454..167ce460b 100644 --- a/backend/testUtils/users.ts +++ b/backend/testUtils/users.ts @@ -8,6 +8,7 @@ export const mockAdmin: UserDTO = { lastName: "One", email: "admin@gmail.com", role: "Admin", + isVerified: true, }; export const mockTeacher: UserDTO = { @@ -18,6 +19,7 @@ export const mockTeacher: UserDTO = { role: "Teacher", grades: [Grade.KINDERGARTEN, Grade.GRADE_1, Grade.GRADE_2, Grade.GRADE_3], currentlyTeachingJM: true, + isVerified: true, }; export const testUsers = [mockAdmin, mockTeacher]; diff --git a/backend/types/index.ts b/backend/types/index.ts index 8e519013d..a91ceb941 100644 --- a/backend/types/index.ts +++ b/backend/types/index.ts @@ -32,15 +32,18 @@ export type UserDTO = { role: Role; grades?: Grade[]; currentlyTeachingJM?: boolean; + isVerified: boolean; }; export type TeacherDTO = UserDTO & { school: string }; -export type CreateUserDTO = Omit & { password: string }; +export type CreateUserDTO = Omit & { + password: string; +}; -export type UpdateUserDTO = Omit; +export type UpdateUserDTO = Omit; -export type RegisterUserDTO = Omit; +export type RegisterUserDTO = Omit; export interface SchoolMetadata { name: string; diff --git a/frontend/src/APIClients/mutations/AuthMutations.ts b/frontend/src/APIClients/mutations/AuthMutations.ts index 6dd2f38ca..fb024f011 100644 --- a/frontend/src/APIClients/mutations/AuthMutations.ts +++ b/frontend/src/APIClients/mutations/AuthMutations.ts @@ -57,24 +57,12 @@ export const RESET_PASSWORD = gql` } `; -export const RESET_PASSWORD_CODE = gql` - mutation ResetPasswordCode($email: String!) { - resetPasswordCode(email: $email) - } -`; - export const VERIFY_EMAIL = gql` mutation VerifyEmail($oobCode: String!) { verifyEmail(oobCode: $oobCode) } `; -export const VERIFY_PASSWORD_RESET = gql` - mutation VerifyPasswordReset($oobCode: String!) { - verifyPasswordReset(oobCode: $oobCode) - } -`; - export const CONFIRM_PASSWORD_RESET = gql` mutation ConfirmPasswordReset($newPassword: String!, $oobCode: String!) { confirmPasswordReset(newPassword: $newPassword, oobCode: $oobCode) diff --git a/frontend/src/APIClients/queries/AuthQueries.ts b/frontend/src/APIClients/queries/AuthQueries.ts new file mode 100644 index 000000000..e5ffde01a --- /dev/null +++ b/frontend/src/APIClients/queries/AuthQueries.ts @@ -0,0 +1,7 @@ +import { gql } from "@apollo/client"; + +export const VERIFY_PASSWORD_RESET_CODE = gql` + query VerifyPasswordResetCode($oobCode: String!) { + verifyPasswordResetCode(oobCode: $oobCode) + } +`; diff --git a/frontend/src/APIClients/queries/UserQueries.ts b/frontend/src/APIClients/queries/UserQueries.ts index accee503c..9be191924 100644 --- a/frontend/src/APIClients/queries/UserQueries.ts +++ b/frontend/src/APIClients/queries/UserQueries.ts @@ -11,6 +11,15 @@ export const GET_USERS_BY_ROLE = gql` } `; +export const GET_USER_VERIFICATION_STATUS = gql` + query GetUserVerificationStatus($id: ID!) { + userVerificationStatus(id: $id) { + id + email + isVerified + } + } +`; export const GET_USER_BY_EMAIL = gql` query GetUserByEmail($email: String!) { userByEmail(email: $email) { @@ -19,6 +28,7 @@ export const GET_USER_BY_EMAIL = gql` lastName email role + isVerified } } `; diff --git a/frontend/src/APIClients/types/UserClientTypes.ts b/frontend/src/APIClients/types/UserClientTypes.ts index ad02f11a0..4211b3da9 100644 --- a/frontend/src/APIClients/types/UserClientTypes.ts +++ b/frontend/src/APIClients/types/UserClientTypes.ts @@ -31,3 +31,10 @@ export type UserResponse = { grades?: Grade[]; currentlyTeachingJM?: boolean; }; + +export type UserVerificationStatus = { + id: string; + email: string; + role: Role; + isVerified: boolean; +}; diff --git a/frontend/src/components/auth/AdminSignupConfirmation.tsx b/frontend/src/components/auth/AdminSignupConfirmation.tsx index 90c60ab0c..b92303114 100644 --- a/frontend/src/components/auth/AdminSignupConfirmation.tsx +++ b/frontend/src/components/auth/AdminSignupConfirmation.tsx @@ -1,12 +1,10 @@ -import React, { useEffect, useState } from "react"; +import type { ReactElement } from "react"; +import React, { useState } from "react"; import { useHistory } from "react-router-dom"; -import { useMutation } from "@apollo/client"; +import { useQuery } from "@apollo/client"; import { Button } from "@chakra-ui/react"; -import { - RESET_PASSWORD_CODE, - VERIFY_PASSWORD_RESET, -} from "../../APIClients/mutations/AuthMutations"; +import { VERIFY_PASSWORD_RESET_CODE } from "../../APIClients/queries/AuthQueries"; import { ADMIN_SIGNUP_IMAGE } from "../../assets/images"; import { ADMIN_LOGIN_PAGE } from "../../constants/Routes"; import LoadingState from "../common/info/LoadingState"; @@ -15,45 +13,23 @@ import EmailActionError from "./email-action/EmailActionError"; import PasswordForm from "./password/PasswordForm"; import AuthWrapper from "./AuthWrapper"; +type AdminSignupConfirmationProps = { + email: string; + resetPasswordOobCode: string; +}; + const AdminSignupConfirmation = ({ email, -}: { - email: string; -}): React.ReactElement => { + resetPasswordOobCode, +}: AdminSignupConfirmationProps): ReactElement => { const [step, setStep] = useState(1); const history = useHistory(); - const [loading, setLoading] = useState(true); - const [oobCode, setOobCode] = React.useState(""); - const [verified, setVerified] = React.useState(false); - - const [verifyPasswordReset] = useMutation<{ verifyPasswordReset: string }>( - VERIFY_PASSWORD_RESET, - { - onCompleted() { - setVerified(true); - setLoading(false); - }, - onError() { - setLoading(false); - }, - }, - ); - - const [resetPasswordCode] = useMutation<{ resetPasswordCode: string }>( - RESET_PASSWORD_CODE, - { - onCompleted: async (data) => { - await verifyPasswordReset({ - variables: { oobCode: data.resetPasswordCode }, - }); - setOobCode(data.resetPasswordCode); - }, - }, - ); - useEffect(() => { - resetPasswordCode({ variables: { email } }); - }, [email, resetPasswordCode]); + const { loading, error } = useQuery<{ + verifyPasswordResetCode: string; + }>(VERIFY_PASSWORD_RESET_CODE, { + variables: { oobCode: resetPasswordOobCode }, + }); const subtitle = step === 1 @@ -63,7 +39,7 @@ const AdminSignupConfirmation = ({ const setPasswordComponent = ( @@ -79,20 +55,22 @@ const AdminSignupConfirmation = ({ ); + if (loading) + return ( + + ); + if (error) return ; + return ( - <> - {loading && } - {verified ? ( - - ) : ( - - )} - + ); }; diff --git a/frontend/src/components/auth/email-action/EmailActionHandler.tsx b/frontend/src/components/auth/email-action/EmailActionHandler.tsx index 8cb9fdca9..6b2cbdbf4 100644 --- a/frontend/src/components/auth/email-action/EmailActionHandler.tsx +++ b/frontend/src/components/auth/email-action/EmailActionHandler.tsx @@ -1,26 +1,27 @@ -import React from "react"; +import React, { type ReactElement } from "react"; -import LoadingState from "../../common/info/LoadingState"; import NotFound from "../../pages/NotFound"; import ResetPasswordHandler from "./ResetPasswordHandler"; import VerifyEmailHandler from "./VerifyEmailHandler"; -const EmailActionHandler = (): React.ReactElement => { +const EmailActionHandler = (): ReactElement => { const urlParams = new URLSearchParams(window.location.search); - const mode = urlParams.get("mode"); - const oobCode: string = urlParams.get("oobCode") ?? ""; + const verifyEmailOobCode = urlParams.get("verifyEmailOobCode") ?? ""; + const resetPasswordOobCode = urlParams.get("resetPasswordOobCode") ?? ""; + const userId = urlParams.get("userId") ?? ""; - switch (mode) { - case "verifyEmail": - return ; - case "resetPassword": - return ; - case null: - return ; - default: - return ; - } + if (verifyEmailOobCode && userId) + return ( + + ); + if (resetPasswordOobCode && !verifyEmailOobCode) + return ; + return ; }; export default EmailActionHandler; diff --git a/frontend/src/components/auth/email-action/ResetPasswordHandler.tsx b/frontend/src/components/auth/email-action/ResetPasswordHandler.tsx index 640e61553..b669a048c 100644 --- a/frontend/src/components/auth/email-action/ResetPasswordHandler.tsx +++ b/frontend/src/components/auth/email-action/ResetPasswordHandler.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from "react"; -import { useMutation, useQuery } from "@apollo/client"; +import React, { useState } from "react"; +import { useQuery } from "@apollo/client"; -import { VERIFY_PASSWORD_RESET } from "../../../APIClients/mutations/AuthMutations"; +import { VERIFY_PASSWORD_RESET_CODE } from "../../../APIClients/queries/AuthQueries"; import { GET_USER_BY_EMAIL } from "../../../APIClients/queries/UserQueries"; import type { Role } from "../../../types/AuthTypes"; import LoadingState from "../../common/info/LoadingState"; @@ -16,10 +16,9 @@ const ResetPasswordHandler = ({ }): React.ReactElement => { const [passwordResetVerified, setPasswordResetVerified] = useState(false); const [email, setEmail] = useState(""); - const [loading, setLoading] = useState(true); const [role, setRole] = React.useState(null); - useQuery(GET_USER_BY_EMAIL, { + const { loading: roleLoading } = useQuery(GET_USER_BY_EMAIL, { variables: { email }, onCompleted: (data) => { setRole(data.userByEmail.role); @@ -27,34 +26,29 @@ const ResetPasswordHandler = ({ skip: !!role || !email, }); - const [verifyPasswordReset] = useMutation<{ verifyPasswordReset: string }>( - VERIFY_PASSWORD_RESET, - { - onCompleted(data: { verifyPasswordReset: string }) { - setEmail(data.verifyPasswordReset); - setPasswordResetVerified(true); - setLoading(false); - }, - onError() { - setLoading(false); - }, + const { loading: verifyCodeLoading } = useQuery<{ + verifyPasswordResetCode: string; + }>(VERIFY_PASSWORD_RESET_CODE, { + onCompleted(data: { verifyPasswordResetCode: string }) { + setEmail(data.verifyPasswordResetCode); + setPasswordResetVerified(true); }, - ); + variables: { oobCode }, + }); - useEffect(() => { - verifyPasswordReset({ variables: { oobCode } }); - }, [oobCode, verifyPasswordReset]); + const isLoading = verifyCodeLoading || roleLoading; + if (isLoading) { + return ( + + ); + } + if (!passwordResetVerified || !role) { + return ; + } - return ( - <> - {loading && } - {passwordResetVerified && role && ( - - )} - {!loading && !passwordResetVerified && ( - - )} - - ); + return ; }; export default ResetPasswordHandler; diff --git a/frontend/src/components/auth/email-action/VerifyEmailHandler.tsx b/frontend/src/components/auth/email-action/VerifyEmailHandler.tsx index 93addddb4..e2ae6a93a 100644 --- a/frontend/src/components/auth/email-action/VerifyEmailHandler.tsx +++ b/frontend/src/components/auth/email-action/VerifyEmailHandler.tsx @@ -1,57 +1,70 @@ -import React, { useEffect, useState } from "react"; +import type { ReactElement } from "react"; +import React, { useState } from "react"; import { useMutation, useQuery } from "@apollo/client"; import { VERIFY_EMAIL } from "../../../APIClients/mutations/AuthMutations"; -import { GET_USER_BY_EMAIL } from "../../../APIClients/queries/UserQueries"; -import type { Role } from "../../../types/AuthTypes"; +import { GET_USER_VERIFICATION_STATUS } from "../../../APIClients/queries/UserQueries"; +import type { UserVerificationStatus } from "../../../APIClients/types/UserClientTypes"; import LoadingState from "../../common/info/LoadingState"; import AdminSignupConfirmation from "../AdminSignupConfirmation"; import TeacherSignupConfirmation from "../teacher-signup/steps/TeacherSignupConfirmation"; import EmailActionError from "./EmailActionError"; +type VerifyEmailHandlerProps = { + verifyEmailOobCode: string; + resetPasswordOobCode: string; + userId: string; +}; + const VerifyEmailHandler = ({ - oobCode, -}: { - oobCode: string; -}): React.ReactElement => { - const [emailVerified, setEmailVerified] = useState(false); + verifyEmailOobCode, + resetPasswordOobCode, + userId, +}: VerifyEmailHandlerProps): ReactElement => { const [email, setEmail] = useState(""); - const [loading, setLoading] = useState(true); - const [role, setRole] = React.useState(null); - - useQuery(GET_USER_BY_EMAIL, { - variables: { email }, - onCompleted: (data) => { - setRole(data.userByEmail.role); - }, - skip: !!role || !email, - }); + const [emailVerified, setEmailVerified] = useState(false); - const [verifyEmail] = useMutation<{ verifyEmail: string }>(VERIFY_EMAIL, { - onCompleted(data: { verifyEmail: string }) { + const [verifyEmail, { loading: isVerifyLoading }] = useMutation<{ + verifyEmail: string; + }>(VERIFY_EMAIL, { + onCompleted(data) { setEmail(data.verifyEmail); setEmailVerified(true); - setLoading(false); }, - onError() { - setLoading(false); + }); + + const { loading: isUserQueryLoading } = useQuery<{ + userVerificationStatus: UserVerificationStatus; + }>(GET_USER_VERIFICATION_STATUS, { + variables: { id: userId }, + onCompleted: (data) => { + setEmail(data.userVerificationStatus.email); + if (data.userVerificationStatus.isVerified) { + setEmailVerified(true); + } else { + verifyEmail({ variables: { oobCode: verifyEmailOobCode } }); + } }, }); - useEffect(() => { - verifyEmail({ variables: { oobCode } }); - }, [oobCode, verifyEmail]); - - return ( - <> - {loading && } - {emailVerified && role === "Teacher" && } - {emailVerified && role === "Admin" && ( - - )} - {!loading && !emailVerified && } - + const isLoading = isUserQueryLoading || isVerifyLoading; + if (isLoading) + return ( + + ); + if (!emailVerified) return ; + + return resetPasswordOobCode ? ( + + ) : ( + ); }; export default VerifyEmailHandler; diff --git a/frontend/src/components/auth/reset-password/ForgotPassword.tsx b/frontend/src/components/auth/reset-password/ForgotPassword.tsx index 29e24d6eb..7602e5004 100644 --- a/frontend/src/components/auth/reset-password/ForgotPassword.tsx +++ b/frontend/src/components/auth/reset-password/ForgotPassword.tsx @@ -49,7 +49,7 @@ const ForgotPassword = ({ try { const { data } = await getUserByEmail({ variables: { email } }); - if (data?.userByEmail?.role === (isAdmin ? "Admin" : "Teacher")) { + if (data?.userByEmail?.role !== (isAdmin ? "Admin" : "Teacher")) { setEmailNotFoundError(true); return; } @@ -58,6 +58,8 @@ const ForgotPassword = ({ return; } + setEmailError(false); + setEmailNotFoundError(false); await resetPassword({ variables: { email } }); };