Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/json/translations/react/en.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low Sensitive Data Finding

PII

More Details
Attribute Value
Data Classifier Contact Details
Data Classifier ID BUILTIN-514

Sampled Examples

Key Value
assistance.contact.helpLine.title1 Frc*o
assistance.contact.questionsAboutListings.title1 D****A
phone 455**9
phone number 455**9

Rule ID: BUILTIN-514


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Sensitive Data Finding

PII

More Details
Attribute Value
Data Classifier PII/Phone Number
Data Classifier ID BUILTIN-32

Sampled Examples

Key Value
phone 455**9
phone number 455**9

Rule ID: BUILTIN-32


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info Sensitive Data Finding

PII

More Details
Attribute Value
Data Classifier PII/Name
Data Classifier ID BUILTIN-125

Sampled Examples

Key Value
assistance.contact.helpLine.title1 Frc*o
assistance.contact.questionsAboutListings.title1 D****A

Rule ID: BUILTIN-125


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate

Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"accountSettings.accountChangesSaved": "Your changes have been saved.",
"accountSettings.accountSettings": "Account Settings",
"accountSettings.checkYourEmail": "We sent you an email. Check your email and follow the link to finish changing your information.",
"accountSettings.changePassword": "Change password",
"accountSettings.contactInformationUpdated": "Contact Information Updated",
"accountSettings.contactInformationUpdatedMessage": "Some of the primary contact information on your application was updated to match your account settings.",
"accountSettings.deleteAria": "Delete application to %{listing}",
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/auth_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def forgot_password
render 'forgot_password'
end

def change_password
@change_password_props = react_app_props
render 'change_password'
end

def reset_password
@reset_password_props = react_app_props
render 'reset_password'
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/packs/react_application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import CreateAnAccount from "../pages/account/create-an-account"
import EnterVerificationCode from "../pages/account/verification-code"
import AddPassword from "../pages/account/add-password"
import AddProfile from "../pages/account/add-profile"
import ChangePassword from "../pages/account/change-password"
import ForgotPassword from "../pages/forgot-password"
import ResetPassword from "../pages/reset-password"
import ListingApplyForm from "../pages/form/listing-apply-form"
Expand Down Expand Up @@ -71,6 +72,7 @@ const PAGE_ROUTES = [
[AddProfile, "/add-profile"],
[ForgotPassword, "/forgot-password"],
[EnterVerificationCode, "/forgot-password/code"],
[ChangePassword, "/change-password"],
[ResetPassword, "/reset-password"],
[HousingCounselors, "/housing-counselors"],
[GetAssistance, "/get-assistance"],
Expand Down
134 changes: 134 additions & 0 deletions app/javascript/pages/account/change-password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/unbound-method */
import { t } from "@bloom-housing/ui-components"
import { Heading } from "@bloom-housing/ui-seeds"
import { useAuth, useSession, useUser } from "@clerk/clerk-react"
import React, { useContext, useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useNavigate } from "react-router"
import UserContext from "../../authentication/context/UserContext"
import { useFeatureFlag } from "../../hooks/useFeatureFlag"
import AuthLayout from "../../layouts/AuthLayout"
import withAppSetup from "../../layouts/withAppSetup"
import { UNLEASH_FLAG } from "../../modules/constants"
import { AppPages, getMyAccountSettingsPath, getSignInPath } from "../../util/routeUtil"
import { ErrorSummaryBanner } from "./components/ErrorSummaryBanner"
import PasswordFieldset, {
passwordFieldsetErrors,
passwordSortOrder,
handleClerkPasswordErrors,
} from "./components/PasswordFieldset"
import { getErrorMessage } from "./components/util"
import { Banner, UpdateForm } from "./settings"
import "./styles/account.scss"

const ChangePasswordPage = () => {
const [loading, setLoading] = useState(false)
const [passwordBanner, setPasswordBanner] = useState(false)
const { profile } = useContext(UserContext)
const { user } = useUser()
const { session } = useSession()

const navigate = useNavigate()
const {
register,
formState: { errors },
handleSubmit,
watch,
setError,
} = useForm({ mode: "onTouched" })

const onSubmit = async (data: { password: string; currentPassword: string }) => {
setLoading(true)
const { password, currentPassword } = data

if (password === "" || !user || !session) {
setLoading(false)
return
}

try {
await session.startVerification({ level: "first_factor" })
await session.attemptFirstFactorVerification({
strategy: "password",
password: currentPassword,
})

await user.updatePassword({
currentPassword,
newPassword: password,
signOutOfOtherSessions: true,
})
setPasswordBanner(true)
void navigate(getMyAccountSettingsPath())
} catch (error) {
setError(...handleClerkPasswordErrors(error))
} finally {
setLoading(false)
}
}
return (
<AuthLayout title={t("accountSettings.changePassword")}>
<Banner
showBanner={passwordBanner}
className="mt-8"
message={t("accountSettings.accountChangesSaved")}
onClose={() => setPasswordBanner(false)}
/>
<ErrorSummaryBanner
errors={errors}
sortOrder={passwordSortOrder}
messageMap={(messageKey) => getErrorMessage(messageKey, passwordFieldsetErrors, true)}
/>
<UpdateForm
onSubmit={handleSubmit(onSubmit)}
loading={loading}
submitLabel={t("accountSettings.savePassword")}
>
<Heading priority={1} size="2xl">
{t("accountSettings.changePassword")}
</Heading>
<PasswordFieldset
register={register}
errors={errors}
watch={watch}
email={profile?.email}
labelText={t("label.password")}
passwordType="accountSettings"
/>
</UpdateForm>
</AuthLayout>
)
}

const ChangePassword = (_props: { assetPaths: unknown }) => {
const navigate = useNavigate()
const { isLoaded, isSignedIn } = useAuth()
const { profile, initialStateLoaded } = useContext(UserContext)
const { unleashFlag: clerkEnabled, flagsReady } = useFeatureFlag(UNLEASH_FLAG.CLERK_AUTH, false)

useEffect(() => {
if (!flagsReady) return
if (!clerkEnabled) {
void navigate(getSignInPath())
return
}
if (!isLoaded) return
if (!isSignedIn) {
void navigate(getSignInPath())
return
}
}, [flagsReady, clerkEnabled, isLoaded, isSignedIn, initialStateLoaded, profile, navigate])

const ready = flagsReady && clerkEnabled && isLoaded && isSignedIn

if (!ready) {
return null
}

return <ChangePasswordPage />
}

export default withAppSetup(ChangePassword, {
useFormTimeout: true,
pageName: AppPages.ChangePassword,
})
18 changes: 16 additions & 2 deletions app/javascript/pages/account/components/PasswordFieldset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"
import { ErrorMessages } from "./ErrorSummaryBanner"
import { ExpandedAccountAxiosError, getErrorMessage, SetErrorArgs } from "./util"
import { getForgotPasswordPath } from "../../../util/routeUtil"
import { isClerkAPIResponseError } from "@clerk/clerk-react/errors"

const PASSWORD_VALIDATION_ERRORS = new Set([
"Password is too short (minimum is 8 characters)",
Expand All @@ -25,9 +26,9 @@ export interface PasswordFieldsetProps {
}

export const handlePasswordServerErrors = (error: ExpandedAccountAxiosError): SetErrorArgs => {
const errorMessages = error.response.data?.errors?.full_messages
const errorMessages = error.response?.data?.errors?.full_messages

if (error.response.status === 422) {
if (error.response?.status === 422 && errorMessages) {
if (errorMessages[0] === "Current password is invalid") {
return [
"currentPassword",
Expand Down Expand Up @@ -75,6 +76,19 @@ export const passwordFieldsetErrors: ErrorMessages = {

export const passwordSortOrder = ["currentPassword", "password"]

export const handleClerkPasswordErrors = (error: unknown): SetErrorArgs => {
if (isClerkAPIResponseError(error)) {
const code = error.errors?.[0]?.code
if (code === "form_password_incorrect") {
return ["currentPassword", { message: "currentPassword:incorrect", shouldFocus: true }]
}
if (code?.startsWith("form_password_")) {
return ["password", { message: "password:complexity", shouldFocus: true }]
}
}
return ["password", { message: "password:server:generic", shouldFocus: true }]
}

const instructionListItem = (
shouldShowValidationInformation: boolean,
validation: boolean,
Expand Down
94 changes: 24 additions & 70 deletions app/javascript/pages/account/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import UserContext from "../../authentication/context/UserContext"
import { Form, DOBFieldValues, t } from "@bloom-housing/ui-components"
import { DeepMap, FieldError, useForm } from "react-hook-form"
import { Card, Alert, Button } from "@bloom-housing/ui-seeds"
import { AppPages, RedirectType } from "../../util/routeUtil"
import { AppPages, getChangePasswordPath, RedirectType } from "../../util/routeUtil"
import { User } from "../../authentication/user"
import Layout from "../../layouts/Layout"
import AccountLayout from "../../layouts/AccountLayout"
Expand All @@ -17,11 +17,6 @@ import EmailFieldset, {
handleEmailServerErrors,
} from "./components/EmailFieldset"
import FormSubmitButton from "./components/FormSubmitButton"
import PasswordFieldset, {
handlePasswordServerErrors,
passwordFieldsetErrors,
passwordSortOrder,
} from "./components/PasswordFieldset"
import NameFieldset, {
handleNameServerErrors,
nameFieldsetErrors,
Expand All @@ -42,7 +37,6 @@ import sharedStyles from "./shared-styles.module.scss"
import {
updateNameOrDOB as apiUpdateNameOrDOB,
updateEmail,
updatePassword,
updateHousingCounselorAccess,
} from "../../api/authApiService"
import { FormHeader, FormSection, getDobStringFromDobObject } from "../../util/accountUtil"
Expand All @@ -54,8 +48,9 @@ import { useFeatureFlag } from "../../hooks/useFeatureFlag"
import { UNLEASH_FLAG } from "../../modules/constants"
import { AccountSettingsPage as MyAccountSettingsPage } from "./account-settings"
import settingsStyles from "./settings.module.scss"
import { useNavigate } from "react-router"

const Banner = ({
export const Banner = ({
showBanner,
className,
message,
Expand All @@ -79,7 +74,7 @@ const Banner = ({
)
}

const UpdateForm = ({
export const UpdateForm = ({
children,
loading,
onSubmit,
Expand Down Expand Up @@ -182,68 +177,27 @@ const EmailSection = ({ user, setUser }: SectionProps) => {
)
}

const PasswordSection = ({ user, setUser }: SectionProps) => {
const [loading, setLoading] = useState(false)
const [passwordBanner, setPasswordBanner] = useState(false)

const {
register,
formState: { errors },
handleSubmit,
reset,
watch,
setError,
} = useForm({ mode: "onTouched" })

const onSubmit = (data: { password: string; currentPassword: string }) => {
setLoading(true)
const { password, currentPassword } = data
if (password === "") {
setLoading(false)
return
}

updatePassword(password, currentPassword)
.then(() => {
const newUser = { ...user, password, currentPassword }
setUser(newUser)
setPasswordBanner(true)
})
.catch((error: ExpandedAccountAxiosError) => setError(...handlePasswordServerErrors(error)))
.finally(() => {
reset({}, { errors: true })
setLoading(false)
})
}
const PasswordSection = () => {
const [loading, _setLoading] = useState(false)
const navigate = useNavigate()

return (
<>
<Banner
showBanner={passwordBanner}
className="mt-8"
message={t("accountSettings.accountChangesSaved")}
onClose={() => setPasswordBanner(false)}
/>
<ErrorSummaryBanner
errors={errors}
sortOrder={passwordSortOrder}
messageMap={(messageKey) => getErrorMessage(messageKey, passwordFieldsetErrors, true)}
/>
<UpdateForm
onSubmit={handleSubmit(onSubmit)}
loading={loading}
submitLabel={t("accountSettings.savePassword")}
>
<PasswordFieldset
register={register}
errors={errors}
watch={watch}
email={user?.email}
labelText={t("label.password")}
passwordType="accountSettings"
/>
</UpdateForm>
</>
<FormSection>
<legend className={"fieldset-legend"}>{t("label.password")}</legend>
<span>••••</span>{" "}
<div className="flex justify-center pt-6">
<Button
loadingMessage={loading ? t("accountSettings.changePassword") : undefined}
type="submit"
variant="primary-outlined"
onClick={() => {
void navigate(getChangePasswordPath())
}}
>
{t("accountSettings.changePassword")}
</Button>
</div>
</FormSection>
)
}

Expand Down Expand Up @@ -597,7 +551,7 @@ const AccountSettings = ({ profile }: { profile: User }) => {
<NameSection user={user} setUser={setUser} handleBanners={handleBanners} />
<DateOfBirthSection user={user} setUser={setUser} />
<EmailSection user={user} setUser={setUser} />
<PasswordSection user={user} setUser={setUser} />
<PasswordSection />
{showHousingCounselorSection && user && (
<HousingCounselorSection user={user} setUser={setUser} />
)}
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/util/routeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const getListingDetailPath = localizedPathGetter("/listings")
export const getCreateAccountPath = localizedPathGetter("/create-account")
export const getVerificationCodePath = localizedPathGetter("/create-account/code")
export const getAddPasswordPath = localizedPathGetter("/add-password")
export const getChangePasswordPath = localizedPathGetter("/change-password")
export const getAddProfilePath = localizedPathGetter("/add-profile")
export const getSignInPath = localizedPathGetter("/sign-in")
export const getSignInCodePath = localizedPathGetter("/sign-in/code")
Expand Down Expand Up @@ -211,6 +212,7 @@ export enum AppPages {
EnterVerificationCode = "enter verification code",
AddPassword = "add password",
AddProfile = "add profile",
ChangePassword = "change password",
ForgotPassword = "forgot password",
ResetPassword = "reset password",
MyAccount = "my account",
Expand Down
1 change: 1 addition & 0 deletions app/views/auth/change_password.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
== react_component 'App', props: @change_password_props
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def matches?(_)
get '(:lang)/create-account/code' => 'auth#enter_verification_code', lang: /(en|es|zh|tl)/
get '(:lang)/add-password' => 'auth#add_password', lang: /(en|es|zh|tl)/
get '(:lang)/add-profile' => 'auth#add_profile', lang: /(en|es|zh|tl)/
get '(:lang)/change-password' => 'auth#change_password', lang: /(en|es|zh|tl)/
get '(:lang)/forgot-password' => 'auth#forgot_password', lang: /(en|es|zh|tl)/
get '(:lang)/reset-password' => 'auth#reset_password', lang: /(en|es|zh|tl)/

Expand Down
Loading