From 5f4c303654aaa0cf7c15c016a8d0cef6bb60fac6 Mon Sep 17 00:00:00 2001 From: cade-exygy <131277283+cade-exygy@users.noreply.github.com> Date: Thu, 8 Feb 2024 09:47:40 -0600 Subject: [PATCH] feat: 3760/redirect to application (#3839) * feat: redirect user after forget password email * refactor: parse query params as strings * feat: redirect user to application after create account * feat: confirmation Modal redirect * feat: redirect after resendConfirmation button * fix: add SHOW_MANDATED_ACCOUNTS var * refactor: check feature toggle first * fix: maintain default behavior when SHOW_MANDATED_ACCOUNTS is FALSE * refactor: remove bend feature toggle * refactor: add helper * refactor: helper function to generate email url * refactor: getListingRedirectUrl helper * style: cleanup * fix: cleanup --- .../core/src/auth/services/user.service.ts | 3 ++- backend/core/src/email/email.service.ts | 3 ++- .../src/shared/utils/get-public-email-url.ts | 18 +++++++++++++++ shared-helpers/src/auth/AuthContext.ts | 23 +++++++++++-------- .../src/utilities/getListingRedirectUrl.ts | 8 +++++++ .../src/views/sign-in/FormSignIn.tsx | 10 ++++++-- .../components/account/ConfirmationModal.tsx | 16 ++++++++++--- .../applications/start/choose-language.tsx | 7 +++--- sites/public/src/pages/create-account.tsx | 22 +++++++++++------- sites/public/src/pages/forgot-password.tsx | 5 ++-- sites/public/src/pages/reset-password.tsx | 11 ++++++++- 11 files changed, 96 insertions(+), 30 deletions(-) create mode 100644 backend/core/src/shared/utils/get-public-email-url.ts create mode 100644 shared-helpers/src/utilities/getListingRedirectUrl.ts diff --git a/backend/core/src/auth/services/user.service.ts b/backend/core/src/auth/services/user.service.ts index fb0a8b235a..21127eec37 100644 --- a/backend/core/src/auth/services/user.service.ts +++ b/backend/core/src/auth/services/user.service.ts @@ -50,6 +50,7 @@ import { Request as ExpressRequest, Response } from "express" import { UserProfileUpdateDto } from "../dto/user-profile.dto" import { Listing } from "../../listings/entities/listing.entity" import { ListingsService } from "../../listings/listings.service" +import { getPublicEmailURL } from "../../shared/utils/get-public-email-url" dayjs.extend(advancedFormat) @@ -621,7 +622,7 @@ export class UserService { } private static getPublicConfirmationUrl(appUrl: string, user: User) { - return `${appUrl}?token=${user.confirmationToken}` + return getPublicEmailURL(appUrl, user.confirmationToken) } private static getPartnersConfirmationUrl(appUrl: string, user: User) { diff --git a/backend/core/src/email/email.service.ts b/backend/core/src/email/email.service.ts index e444e35ddb..23f67a51c4 100644 --- a/backend/core/src/email/email.service.ts +++ b/backend/core/src/email/email.service.ts @@ -19,6 +19,7 @@ import { Language } from "../shared/types/language-enum" import { JurisdictionsService } from "../jurisdictions/services/jurisdictions.service" import { Translation } from "../translations/entities/translation.entity" import { formatLocalDate } from "../shared/utils/format-local-date" +import { getPublicEmailURL } from "../shared/utils/get-public-email-url" type EmailAttachmentData = { data: string @@ -207,8 +208,8 @@ export class EmailService { const jurisdiction = await this.getUserJurisdiction(user) void (await this.loadTranslations(jurisdiction, user.language)) const compiledTemplate = this.template("forgot-password") - const resetUrl = `${appUrl}/reset-password?token=${user.resetToken}` + const resetUrl = getPublicEmailURL(appUrl, user.resetToken, "/reset-password") if (this.configService.get("NODE_ENV") == "production") { Logger.log( `Preparing to send a forget password email to ${user.email} from ${jurisdiction.emailFromAddress}...` diff --git a/backend/core/src/shared/utils/get-public-email-url.ts b/backend/core/src/shared/utils/get-public-email-url.ts new file mode 100644 index 0000000000..2e6a2912a0 --- /dev/null +++ b/backend/core/src/shared/utils/get-public-email-url.ts @@ -0,0 +1,18 @@ +/** + * Creates a email URL object from passed url applies redirectUrl and listingId query params if they exist + * If they do not exist, the return value will be the email url with just the necessary token + */ + +export const getPublicEmailURL = (url: string, token: string, actionPath?: string): string => { + const urlObj = new URL(url) + + const redirectUrl = urlObj.searchParams.get("redirectUrl") + const listingId = urlObj.searchParams.get("listingId") + + let emailUrl = `${urlObj.origin}${urlObj.pathname}/${actionPath ? actionPath : ""}?token=${token}` + + if (!!redirectUrl && !!listingId) { + emailUrl = emailUrl.concat(`&redirectUrl=${redirectUrl}&listingId=${listingId}`) + } + return emailUrl +} diff --git a/shared-helpers/src/auth/AuthContext.ts b/shared-helpers/src/auth/AuthContext.ts index f3bff26f8f..687e4867f3 100644 --- a/shared-helpers/src/auth/AuthContext.ts +++ b/shared-helpers/src/auth/AuthContext.ts @@ -36,6 +36,7 @@ import qs from "qs" import axiosStatic from "axios" import { ConfigContext } from "./ConfigContext" import { createAction, createReducer } from "typesafe-actions" +import { getListingRedirectUrl } from "../utilities/getListingRedirectUrl" type ContextProps = { amiChartsService: AmiChartsService @@ -65,9 +66,9 @@ type ContextProps = { ) => Promise signOut: () => void confirmAccount: (token: string) => Promise - forgotPassword: (email: string) => Promise - createUser: (user: UserCreate) => Promise - resendConfirmation: (email: string) => Promise + forgotPassword: (email: string, listingIdRedirect?: string) => Promise + createUser: (user: UserCreate, listingIdRedirect?: string) => Promise + resendConfirmation: (email: string, listingIdRedirect?: string) => Promise initialStateLoaded: boolean loading: boolean profile?: User @@ -289,33 +290,37 @@ export const AuthProvider: FunctionComponent = ({ child dispatch(stopLoading()) } }, - createUser: async (user: UserCreate) => { + createUser: async (user: UserCreate, listingIdRedirect) => { dispatch(startLoading()) + const appUrl = getListingRedirectUrl(listingIdRedirect) try { const response = await userService?.create({ - body: { ...user, appUrl: window.location.origin }, + body: { ...user, appUrl }, }) return response } finally { dispatch(stopLoading()) } }, - resendConfirmation: async (email: string) => { + resendConfirmation: async (email: string, listingIdRedirect) => { dispatch(startLoading()) + const appUrl = getListingRedirectUrl(listingIdRedirect) try { const response = await userService?.resendConfirmation({ - body: { email, appUrl: window.location.origin }, + body: { email, appUrl }, }) return response } finally { dispatch(stopLoading()) } }, - forgotPassword: async (email) => { + forgotPassword: async (email, listingIdRedirect) => { dispatch(startLoading()) try { + const appUrl = getListingRedirectUrl(listingIdRedirect) + const response = await userService?.forgotPassword({ - body: { email, appUrl: window.location.origin }, + body: { email, appUrl }, }) return response?.message } finally { diff --git a/shared-helpers/src/utilities/getListingRedirectUrl.ts b/shared-helpers/src/utilities/getListingRedirectUrl.ts new file mode 100644 index 0000000000..5856020621 --- /dev/null +++ b/shared-helpers/src/utilities/getListingRedirectUrl.ts @@ -0,0 +1,8 @@ +export const getListingRedirectUrl = ( + listingIdRedirect: string | undefined, + path: string = window.location.origin +) => { + return process.env.showMandatedAccounts && listingIdRedirect + ? `${path}?redirectUrl=/applications/start/choose-language&listingId=${listingIdRedirect}` + : path +} diff --git a/shared-helpers/src/views/sign-in/FormSignIn.tsx b/shared-helpers/src/views/sign-in/FormSignIn.tsx index cd2c62bdb4..19c5df2d3a 100644 --- a/shared-helpers/src/views/sign-in/FormSignIn.tsx +++ b/shared-helpers/src/views/sign-in/FormSignIn.tsx @@ -4,6 +4,8 @@ import { Button } from "@bloom-housing/ui-seeds" import { FormSignInErrorBox } from "./FormSignInErrorBox" import { NetworkStatus } from "../../auth/catchNetworkError" import type { UseFormMethods } from "react-hook-form" +import { useRouter } from "next/router" +import { getListingRedirectUrl } from "../../utilities/getListingRedirectUrl" export type FormSignInProps = { control: FormSignInControl @@ -34,6 +36,10 @@ const FormSignIn = ({ window.scrollTo(0, 0) } const { LinkComponent } = useContext(NavigationContext) + const router = useRouter() + const listingIdRedirect = router.query?.listingId as string + const forgetPasswordURL = getListingRedirectUrl(listingIdRedirect, "/forgot-password") + const createAccountUrl = getListingRedirectUrl(listingIdRedirect, "/create-account") return ( @@ -60,7 +66,7 @@ const FormSignIn = ({ /> @@ -88,7 +94,7 @@ const FormSignIn = ({

{t("authentication.createAccount.noAccount")}

-
diff --git a/sites/public/src/components/account/ConfirmationModal.tsx b/sites/public/src/components/account/ConfirmationModal.tsx index bd671b6055..06bf5b1e2b 100644 --- a/sites/public/src/components/account/ConfirmationModal.tsx +++ b/sites/public/src/components/account/ConfirmationModal.tsx @@ -27,7 +27,8 @@ const ConfirmationModal = (props: ConfirmationModalProps) => { const onSubmit = async ({ email }) => { try { - await resendConfirmation(email) + const listingId = router.query?.listingId as string + await resendConfirmation(email, listingId) setSiteAlertMessage(t(`authentication.createAccount.emailSent`), "success") setOpenModal(false) @@ -39,12 +40,21 @@ const ConfirmationModal = (props: ConfirmationModalProps) => { } useEffect(() => { + const redirectUrl = router.query?.redirectUrl as string + const listingId = router.query?.listingId as string + + const routerRedirectUrl = + process.env.showMandatedAccounts && redirectUrl && listingId + ? `${redirectUrl}` + : "/account/dashboard" if (router?.query?.token && !profile) { confirmAccount(router.query.token.toString()) .then(() => { void router.push({ - pathname: "/account/dashboard", - query: { alert: `authentication.createAccount.accountConfirmed` }, + pathname: routerRedirectUrl, + query: process.env.showMandatedAccounts + ? { listingId: listingId } + : { alert: `authentication.createAccount.accountConfirmed` }, }) window.scrollTo(0, 0) }) diff --git a/sites/public/src/pages/applications/start/choose-language.tsx b/sites/public/src/pages/applications/start/choose-language.tsx index bbce836a0b..9bea4e9f1f 100644 --- a/sites/public/src/pages/applications/start/choose-language.tsx +++ b/sites/public/src/pages/applications/start/choose-language.tsx @@ -55,8 +55,9 @@ const ApplicationChooseLanguage = () => { conductor.reset() if (!router.isReady && !listingId) return if (router.isReady && !listingId) { - void router.push("/") - return + return process.env.showMandatedAccounts && initialStateLoaded && profile + ? undefined + : void router.push("/") } if (!context.listing || context.listing.id !== listingId) { @@ -65,7 +66,7 @@ const ApplicationChooseLanguage = () => { conductor.listing = context.listing setListing(context.listing) } - }, [router, conductor, context, listingId]) + }, [router, conductor, context, listingId, initialStateLoaded, profile]) useEffect(() => { if (listing && router.isReady) { diff --git a/sites/public/src/pages/create-account.tsx b/sites/public/src/pages/create-account.tsx index 1b6bc88894..0bbab86a27 100644 --- a/sites/public/src/pages/create-account.tsx +++ b/sites/public/src/pages/create-account.tsx @@ -32,6 +32,7 @@ export default () => { const [openModal, setOpenModal] = useState(false) const router = useRouter() const language = router.locale + const listingId = router.query?.listingId as string const email = useRef({}) const password = useRef({}) email.current = watch("email", "") @@ -48,11 +49,16 @@ export default () => { const onSubmit = async (data) => { try { const { dob, ...rest } = data - await createUser({ - ...rest, - dob: dayjs(`${dob.birthYear}-${dob.birthMonth}-${dob.birthDay}`), - language, - }) + const listingIdRedirect = + process.env.showMandatedAccounts && listingId ? listingId : undefined + await createUser( + { + ...rest, + dob: dayjs(`${dob.birthYear}-${dob.birthMonth}-${dob.birthDay}`), + language, + }, + listingIdRedirect + ) setOpenModal(true) } catch (err) { @@ -255,14 +261,14 @@ export default () => { email: email.current, })} onClose={() => { - void router.push("/") + void router.push("/sign-in") window.scrollTo(0, 0) }} actions={[