Skip to content

Commit

Permalink
feat: 3760/redirect to application (#3839)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cade-exygy authored Feb 8, 2024
1 parent 1c99bcf commit 5f4c303
Show file tree
Hide file tree
Showing 11 changed files with 96 additions and 30 deletions.
3 changes: 2 additions & 1 deletion backend/core/src/auth/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion backend/core/src/email/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string>("NODE_ENV") == "production") {
Logger.log(
`Preparing to send a forget password email to ${user.email} from ${jurisdiction.emailFromAddress}...`
Expand Down
18 changes: 18 additions & 0 deletions backend/core/src/shared/utils/get-public-email-url.ts
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 14 additions & 9 deletions shared-helpers/src/auth/AuthContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,9 +66,9 @@ type ContextProps = {
) => Promise<User | undefined>
signOut: () => void
confirmAccount: (token: string) => Promise<User | undefined>
forgotPassword: (email: string) => Promise<string | undefined>
createUser: (user: UserCreate) => Promise<UserBasic | undefined>
resendConfirmation: (email: string) => Promise<Status | undefined>
forgotPassword: (email: string, listingIdRedirect?: string) => Promise<string | undefined>
createUser: (user: UserCreate, listingIdRedirect?: string) => Promise<UserBasic | undefined>
resendConfirmation: (email: string, listingIdRedirect?: string) => Promise<Status | undefined>
initialStateLoaded: boolean
loading: boolean
profile?: User
Expand Down Expand Up @@ -289,33 +290,37 @@ export const AuthProvider: FunctionComponent<React.PropsWithChildren> = ({ 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 {
Expand Down
8 changes: 8 additions & 0 deletions shared-helpers/src/utilities/getListingRedirectUrl.ts
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 8 additions & 2 deletions shared-helpers/src/views/sign-in/FormSignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<FormCard>
Expand All @@ -60,7 +66,7 @@ const FormSignIn = ({
/>

<aside className="float-right text-sm font-semibold">
<LinkComponent href="/forgot-password">
<LinkComponent href={forgetPasswordURL}>
{t("authentication.signIn.forgotPassword")}
</LinkComponent>
</aside>
Expand Down Expand Up @@ -88,7 +94,7 @@ const FormSignIn = ({
<div className="form-card__group text-center border-t">
<h2 className="mb-6">{t("authentication.createAccount.noAccount")}</h2>

<Button variant="primary-outlined" href="/create-account">
<Button variant="primary-outlined" href={createAccountUrl}>
{t("account.createAccount")}
</Button>
</div>
Expand Down
16 changes: 13 additions & 3 deletions sites/public/src/components/account/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
})
Expand Down
7 changes: 4 additions & 3 deletions sites/public/src/pages/applications/start/choose-language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
22 changes: 14 additions & 8 deletions sites/public/src/pages/create-account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default () => {
const [openModal, setOpenModal] = useState<boolean>(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", "")
Expand All @@ -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) {
Expand Down Expand Up @@ -255,14 +261,14 @@ export default () => {
email: email.current,
})}
onClose={() => {
void router.push("/")
void router.push("/sign-in")
window.scrollTo(0, 0)
}}
actions={[
<Button
variant="primary"
onClick={() => {
void router.push("/")
void router.push("/sign-in")
window.scrollTo(0, 0)
}}
size="sm"
Expand All @@ -274,7 +280,7 @@ export default () => {
disabled={confirmationResent}
onClick={() => {
setConfirmationResent(true)
void resendConfirmation(email.current.toString())
void resendConfirmation(email.current.toString(), listingId)
}}
size="sm"
>
Expand Down
5 changes: 3 additions & 2 deletions sites/public/src/pages/forgot-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ const ForgotPassword = () => {

const onSubmit = async (data: { email: string }) => {
const { email } = data

const listingId = router.query?.listingId as string
const listingIdRedirect = listingId && process.env.showMandatedAccounts ? listingId : undefined
try {
await forgotPassword(email)
await forgotPassword(email, listingIdRedirect)
} catch (error) {
const { status } = error.response || {}
determineNetworkError(status, error)
Expand Down
11 changes: 10 additions & 1 deletion sites/public/src/pages/reset-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,16 @@ const ResetPassword = () => {
try {
const user = await resetPassword(token.toString(), password, passwordConfirmation)
setSiteAlertMessage(t(`authentication.signIn.success`, { name: user.firstName }), "success")
await router.push("/account/applications")

const redirectUrl = router.query?.redirectUrl as string
const listingId = router.query?.listingId as string

const routerRedirectUrl =
process.env.showMandatedAccounts && redirectUrl && listingId
? `${redirectUrl}?listingId=${listingId}`
: "/account/applications"

await router.push(routerRedirectUrl)
} catch (err) {
const { status, data } = err.response || {}
if (status === 400) {
Expand Down

0 comments on commit 5f4c303

Please sign in to comment.