diff --git a/api/Procfile b/api/Procfile
index 36c6b6bdf1..482d54025a 100644
--- a/api/Procfile
+++ b/api/Procfile
@@ -1 +1 @@
-web: yarn start:prod
+web: yarn db:migration:run && yarn start:prod
diff --git a/api/prisma/migrations/05_single_use_code_translation_updates/migration.sql b/api/prisma/migrations/05_single_use_code_translation_updates/migration.sql
index 06df833ecb..1fab055382 100644
--- a/api/prisma/migrations/05_single_use_code_translation_updates/migration.sql
+++ b/api/prisma/migrations/05_single_use_code_translation_updates/migration.sql
@@ -5,6 +5,8 @@ SET translations = jsonb_set(translations, '{singleUseCodeEmail}', '{"greeting":
WHERE jurisdiction_id IS NULL
and language = 'en';
+
UPDATE translations
- SET translations = jsonb_set(translations, '{mfaCodeEmail, mfaCode}', '{"mfaCode": "Your access code is: %{singleUseCode}"}')
+SET translations = jsonb_set(translations, '{mfaCodeEmail, mfaCode}', '"Your access code is: %{singleUseCode}"')
WHERE language = 'en';
+
diff --git a/api/prisma/seed-helpers/translation-factory.ts b/api/prisma/seed-helpers/translation-factory.ts
index 9158ae499f..3b377d3df8 100644
--- a/api/prisma/seed-helpers/translation-factory.ts
+++ b/api/prisma/seed-helpers/translation-factory.ts
@@ -1,166 +1,204 @@
import { LanguagesEnum, Prisma } from '@prisma/client';
-const translations = (jurisdictionName?: string) => ({
- t: {
- hello: 'Hello',
- seeListing: 'See Listing',
- partnersPortal: 'Partners Portal',
- viewListing: 'View Listing',
- editListing: 'Edit Listing',
- reviewListing: 'Review Listing',
- },
- footer: {
- line1: `${jurisdictionName || 'Bloom'}`,
- line2: '',
- thankYou: 'Thank you',
- footer: `${jurisdictionName || 'Bloom Housing'}`,
- },
- header: {
- logoUrl:
- 'https://res.cloudinary.com/exygy/image/upload/w_400,c_limit,q_65/dev/bloom_logo_generic_zgb4sg.jpg',
- logoTitle: 'Bloom Housing Portal',
- },
- invite: {
- hello: 'Welcome to the Partners Portal',
- confirmMyAccount: 'Confirm my account',
- inviteManageListings:
- 'You will now be able to manage listings and applications that you are a part of from one centralized location.',
- inviteWelcomeMessage: 'Welcome to the Partners Portal at %{appUrl}.',
- toCompleteAccountCreation:
- 'To complete your account creation, please click the link below:',
- },
- register: {
- welcome: 'Welcome',
- welcomeMessage:
- 'Thank you for setting up your account on %{appUrl}. It will now be easier for you to start, save, and submit online applications for listings that appear on the site.',
- confirmMyAccount: 'Confirm my account',
- toConfirmAccountMessage:
- 'To complete your account creation, please click the link below:',
- },
- changeEmail: {
- message: 'An email address change has been requested for your account.',
- changeMyEmail: 'Confirm email change',
- onChangeEmailMessage:
- 'To confirm the change to your email address, please click the link below:',
- },
- confirmation: {
- subject: 'Your Application Confirmation',
- eligible: {
- fcfs: 'Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.',
- lottery:
- 'Once the application period closes, eligible applicants will be placed in order based on lottery rank order.',
- waitlist:
- 'Eligible applicants will be placed on the waitlist on a first come first serve basis until waitlist spots are filled.',
- fcfsPreference:
- 'Housing preferences, if applicable, will affect first come first serve order.',
- waitlistContact:
- 'You may be contacted while on the waitlist to confirm that you wish to remain on the waitlist.',
- lotteryPreference:
- 'Housing preferences, if applicable, will affect lottery rank order.',
- waitlistPreference:
- 'Housing preferences, if applicable, will affect waitlist order.',
- },
- interview:
- 'If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.',
- whatToExpect: {
- FCFS: 'Applicants will be contacted by the property agent on a first come first serve basis until vacancies are filled.',
- lottery:
- 'Applicants will be contacted by the agent in lottery rank order until vacancies are filled.',
- noLottery:
- 'Applicants will be contacted by the agent in waitlist order until vacancies are filled.',
- },
- whileYouWait:
- 'While you wait, there are things you can do to prepare for potential next steps and future opportunities.',
- shouldBeChosen:
- 'Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.',
- whatHappensNext: 'What happens next?',
- whatToExpectNext: 'What to expect next:',
- needToMakeUpdates: 'Need to make updates?',
- applicationsClosed: 'Application
closed',
- applicationsRanked: 'Application
ranked',
- eligibleApplicants: {
- FCFS: 'Eligible applicants will be placed in order based on first come first serve basis.',
- lottery:
- 'Eligible applicants will be placed in order based on preference and lottery rank.',
- lotteryDate: 'The lottery will be held on %{lotteryDate}.',
- },
- applicationReceived: 'Application
received',
- prepareForNextSteps: 'Prepare for next steps',
- thankYouForApplying:
- 'Thanks for applying. We have received your application for',
- readHowYouCanPrepare: 'Read about how you can prepare for next steps',
- yourConfirmationNumber: 'Your Confirmation Number',
- applicationPeriodCloses:
- 'Once the application period closes, the property manager will begin processing applications.',
- contactedForAnInterview:
- 'If you are contacted for an interview, you will need to fill out a more detailed application and provide supporting documents.',
- gotYourConfirmationNumber: 'We got your application for',
- },
- leasingAgent: {
- officeHours: 'Office Hours:',
- propertyManager: 'Property Manager',
- contactAgentToUpdateInfo:
- 'If you need to update information on your application, do not apply again. Instead, contact the agent for this listing.',
- },
- mfaCodeEmail: {
- message: 'Access code for your account has been requested.',
- mfaCode: 'Your access code is: %{singleUseCode}',
- },
- forgotPassword: {
- subject: 'Forgot your password?',
- callToAction:
- 'If you did make this request, please click on the link below to reset your password:',
- passwordInfo:
- "Your password won't change until you access the link above and create a new one.",
- resetRequest:
- 'A request to reset your Bloom Housing Portal website password for %{appUrl} has recently been made.',
- ignoreRequest: "If you didn't request this, please ignore this email.",
- changePassword: 'Change my password',
- },
- requestApproval: {
- header: 'Listing approval requested',
- partnerRequest:
- 'A Partner has submitted an approval request to publish the %{listingName} listing.',
- logInToReviewStart: 'Please log into the',
- logInToReviewEnd:
- 'and navigate to the listing detail page to review and publish.',
- accessListing:
- 'To access the listing after logging in, please click the link below',
- },
- changesRequested: {
- header: 'Listing changes requested',
- adminRequestStart:
- 'An administrator is requesting changes to the %{listingName} listing. Please log into the',
- adminRequestEnd:
- 'and navigate to the listing detail page to view the request and edit the listing.',
- },
- listingApproved: {
- header: 'New published listing',
- adminApproved:
- 'The %{listingName} listing has been approved and published by an administrator.',
- viewPublished:
- 'To view the published listing, please click on the link below',
- },
- csvExport: {
- body: 'The attached file is %{fileDescription}. If you have any questions, please reach out to your administrator.',
- hello: 'Hello,',
- title: '%{title}',
- },
- singleUseCodeEmail: {
- greeting: 'Hi',
- message:
- 'Use the following code to sign in to your %{jurisdictionName} account. This code will be valid for 5 minutes. Never share this code.',
- singleUseCode: '%{singleUseCode}',
- },
-});
+const translations = (jurisdictionName?: string, language?: LanguagesEnum) => {
+ if (!language || language === LanguagesEnum.en) {
+ return {
+ t: {
+ hello: 'Hello',
+ seeListing: 'See Listing',
+ partnersPortal: 'Partners Portal',
+ viewListing: 'View Listing',
+ editListing: 'Edit Listing',
+ reviewListing: 'Review Listing',
+ },
+ footer: {
+ line1: `${jurisdictionName || 'Bloom'}`,
+ line2: '',
+ thankYou: 'Thank you',
+ footer: `${jurisdictionName || 'Bloom Housing'}`,
+ },
+ header: {
+ logoUrl:
+ 'https://res.cloudinary.com/exygy/image/upload/w_400,c_limit,q_65/dev/bloom_logo_generic_zgb4sg.jpg',
+ logoTitle: 'Bloom Housing Portal',
+ },
+ invite: {
+ hello: 'Welcome to the Partners Portal',
+ confirmMyAccount: 'Confirm my account',
+ inviteManageListings:
+ 'You will now be able to manage listings and applications that you are a part of from one centralized location.',
+ inviteWelcomeMessage: 'Welcome to the Partners Portal at %{appUrl}.',
+ toCompleteAccountCreation:
+ 'To complete your account creation, please click the link below:',
+ },
+ register: {
+ welcome: 'Welcome',
+ welcomeMessage:
+ 'Thank you for setting up your account on %{appUrl}. It will now be easier for you to start, save, and submit online applications for listings that appear on the site.',
+ confirmMyAccount: 'Confirm my account',
+ toConfirmAccountMessage:
+ 'To complete your account creation, please click the link below:',
+ },
+ changeEmail: {
+ message: 'An email address change has been requested for your account.',
+ changeMyEmail: 'Confirm email change',
+ onChangeEmailMessage:
+ 'To confirm the change to your email address, please click the link below:',
+ },
+ confirmation: {
+ subject: 'Your Application Confirmation',
+ eligible: {
+ fcfs: 'Eligible applicants will be contacted on a first come first serve basis until vacancies are filled.',
+ lottery:
+ 'Once the application period closes, eligible applicants will be placed in order based on lottery rank order.',
+ waitlist:
+ 'Eligible applicants will be placed on the waitlist on a first come first serve basis until waitlist spots are filled.',
+ fcfsPreference:
+ 'Housing preferences, if applicable, will affect first come first serve order.',
+ waitlistContact:
+ 'You may be contacted while on the waitlist to confirm that you wish to remain on the waitlist.',
+ lotteryPreference:
+ 'Housing preferences, if applicable, will affect lottery rank order.',
+ waitlistPreference:
+ 'Housing preferences, if applicable, will affect waitlist order.',
+ },
+ interview:
+ 'If you are contacted for an interview, you will be asked to fill out a more detailed application and provide supporting documents.',
+ whatToExpect: {
+ FCFS: 'Applicants will be contacted by the property agent on a first come first serve basis until vacancies are filled.',
+ lottery:
+ 'Applicants will be contacted by the agent in lottery rank order until vacancies are filled.',
+ noLottery:
+ 'Applicants will be contacted by the agent in waitlist order until vacancies are filled.',
+ },
+ whileYouWait:
+ 'While you wait, there are things you can do to prepare for potential next steps and future opportunities.',
+ shouldBeChosen:
+ 'Should your application be chosen, be prepared to fill out a more detailed application and provide required supporting documents.',
+ whatHappensNext: 'What happens next?',
+ whatToExpectNext: 'What to expect next:',
+ needToMakeUpdates: 'Need to make updates?',
+ applicationsClosed: 'Application
closed',
+ applicationsRanked: 'Application
ranked',
+ eligibleApplicants: {
+ FCFS: 'Eligible applicants will be placed in order based on first come first serve basis.',
+ lottery:
+ 'Eligible applicants will be placed in order based on preference and lottery rank.',
+ lotteryDate: 'The lottery will be held on %{lotteryDate}.',
+ },
+ applicationReceived: 'Application
received',
+ prepareForNextSteps: 'Prepare for next steps',
+ thankYouForApplying:
+ 'Thanks for applying. We have received your application for',
+ readHowYouCanPrepare: 'Read about how you can prepare for next steps',
+ yourConfirmationNumber: 'Your Confirmation Number',
+ applicationPeriodCloses:
+ 'Once the application period closes, the property manager will begin processing applications.',
+ contactedForAnInterview:
+ 'If you are contacted for an interview, you will need to fill out a more detailed application and provide supporting documents.',
+ gotYourConfirmationNumber: 'We got your application for',
+ },
+ leasingAgent: {
+ officeHours: 'Office Hours:',
+ propertyManager: 'Property Manager',
+ contactAgentToUpdateInfo:
+ 'If you need to update information on your application, do not apply again. Instead, contact the agent for this listing.',
+ },
+ mfaCodeEmail: {
+ message: 'Access code for your account has been requested.',
+ mfaCode: 'Your access code is: %{singleUseCode}',
+ },
+ forgotPassword: {
+ subject: 'Forgot your password?',
+ callToAction:
+ 'If you did make this request, please click on the link below to reset your password:',
+ passwordInfo:
+ "Your password won't change until you access the link above and create a new one.",
+ resetRequest:
+ 'A request to reset your Bloom Housing Portal website password for %{appUrl} has recently been made.',
+ ignoreRequest: "If you didn't request this, please ignore this email.",
+ changePassword: 'Change my password',
+ },
+ requestApproval: {
+ header: 'Listing approval requested',
+ partnerRequest:
+ 'A Partner has submitted an approval request to publish the %{listingName} listing.',
+ logInToReviewStart: 'Please log into the',
+ logInToReviewEnd:
+ 'and navigate to the listing detail page to review and publish.',
+ accessListing:
+ 'To access the listing after logging in, please click the link below',
+ },
+ changesRequested: {
+ header: 'Listing changes requested',
+ adminRequestStart:
+ 'An administrator is requesting changes to the %{listingName} listing. Please log into the',
+ adminRequestEnd:
+ 'and navigate to the listing detail page to view the request and edit the listing.',
+ },
+ listingApproved: {
+ header: 'New published listing',
+ adminApproved:
+ 'The %{listingName} listing has been approved and published by an administrator.',
+ viewPublished:
+ 'To view the published listing, please click on the link below',
+ },
+ csvExport: {
+ body: 'The attached file is %{fileDescription}. If you have any questions, please reach out to your administrator.',
+ hello: 'Hello,',
+ title: '%{title}',
+ },
+ singleUseCodeEmail: {
+ greeting: 'Hi',
+ message:
+ 'Use the following code to sign in to your %{jurisdictionName} account. This code will be valid for 5 minutes. Never share this code.',
+ singleUseCode: '%{singleUseCode}',
+ },
+ };
+ } else if (language === LanguagesEnum.es) {
+ return {
+ t: { seeListing: 'VER EL LISTADO' },
+ footer: {
+ line1: `${jurisdictionName || 'Bloom'}`,
+ line2: '',
+ },
+ confirmation: {
+ eligible: {
+ waitlist:
+ 'Los solicitantes que reúnan los requisitos quedarán en la lista de espera por orden de recepción de solicitud hasta que se cubran todos los lugares.',
+ waitlistContact:
+ 'Es posible que se comuniquen con usted mientras esté en la lista de espera para confirmar que desea permanecer en la lista.',
+ waitlistPreference:
+ 'Las preferencias de vivienda, si corresponde, afectarán al orden de la lista de espera.',
+ },
+ interview:
+ 'Si se comunican con usted para una entrevista, se le pedirá que complete una solicitud más detallada y presente documentos de respaldo.',
+ whatHappensNext: '¿Qué sucede luego?',
+ needToMakeUpdates: '¿Necesita hacer modificaciones?',
+ applicationsClosed: 'Solicitud
cerrada',
+ applicationsRanked: 'Solicitud
clasificada',
+ applicationReceived: 'Aplicación
recibida',
+ yourConfirmationNumber: 'Su número de confirmación',
+ gotYourConfirmationNumber: 'Recibimos tu solicitud para:',
+ },
+ leasingAgent: {
+ officeHours: 'Horario de atención',
+ propertyManager: 'Administrador de propiedades',
+ contactAgentToUpdateInfo:
+ 'Si necesita modificar información en su solicitud, no haga una solicitud nueva. Comuníquese con el agente de este listado.',
+ },
+ };
+ }
+};
export const translationFactory = (
jurisdictionId?: string,
jurisdictionName?: string,
+ language?: LanguagesEnum,
): Prisma.TranslationsCreateInput => {
return {
- language: LanguagesEnum.en,
- translations: translations(jurisdictionName),
+ language: language || LanguagesEnum.en,
+ translations: translations(jurisdictionName, language),
jurisdictions: jurisdictionId
? {
connect: {
diff --git a/api/prisma/seed-staging.ts b/api/prisma/seed-staging.ts
index 2b68d65297..627b7ceea9 100644
--- a/api/prisma/seed-staging.ts
+++ b/api/prisma/seed-staging.ts
@@ -1,6 +1,7 @@
import {
ApplicationAddressTypeEnum,
ApplicationMethodsTypeEnum,
+ LanguagesEnum,
ListingsStatusEnum,
MultiselectQuestions,
MultiselectQuestionsApplicationSectionEnum,
@@ -92,6 +93,9 @@ export const stagingSeed = async (
await prismaClient.translations.create({
data: translationFactory(jurisdiction.id, jurisdiction.name),
});
+ await prismaClient.translations.create({
+ data: translationFactory(undefined, undefined, LanguagesEnum.es),
+ });
await prismaClient.translations.create({
data: translationFactory(),
});
diff --git a/api/src/passports/mfa.strategy.ts b/api/src/passports/mfa.strategy.ts
index 5afb8f078b..4a510090f2 100644
--- a/api/src/passports/mfa.strategy.ts
+++ b/api/src/passports/mfa.strategy.ts
@@ -63,7 +63,18 @@ export class MfaStrategy extends PassportStrategy(Strategy, 'mfa') {
Number(process.env.AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS),
Number(process.env.AUTH_LOCK_LOGIN_COOLDOWN),
);
- if (!rawUser.confirmedAt) {
+ if (!(await isPasswordValid(rawUser.passwordHash, dto.password))) {
+ // if incoming password does not match
+ await this.updateFailedLoginCount(
+ rawUser.failedLoginAttemptsCount + 1,
+ rawUser.id,
+ );
+ throw new UnauthorizedException({
+ failureCountRemaining:
+ Number(process.env.AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS) -
+ rawUser.failedLoginAttemptsCount,
+ });
+ } else if (!rawUser.confirmedAt) {
// if user is not confirmed already
throw new UnauthorizedException(
`user ${rawUser.id} attempted to login, but is not confirmed`,
@@ -78,17 +89,6 @@ export class MfaStrategy extends PassportStrategy(Strategy, 'mfa') {
throw new UnauthorizedException(
`user ${rawUser.id} attempted to login, but password is no longer valid`,
);
- } else if (!(await isPasswordValid(rawUser.passwordHash, dto.password))) {
- // if incoming password does not match
- await this.updateFailedLoginCount(
- rawUser.failedLoginAttemptsCount + 1,
- rawUser.id,
- );
- throw new UnauthorizedException({
- failureCountRemaining:
- Number(process.env.AUTH_LOCK_LOGIN_AFTER_FAILED_ATTEMPTS) -
- rawUser.failedLoginAttemptsCount,
- });
}
if (!rawUser.mfaEnabled) {
diff --git a/api/test/unit/passports/mfa.strategy.spec.ts b/api/test/unit/passports/mfa.strategy.spec.ts
index 2d5f684623..35bad3d0e5 100644
--- a/api/test/unit/passports/mfa.strategy.spec.ts
+++ b/api/test/unit/passports/mfa.strategy.spec.ts
@@ -88,6 +88,7 @@ describe('Testing mfa strategy', () => {
lastLoginAt: new Date(),
failedLoginAttemptsCount: 0,
confirmedAt: null,
+ passwordHash: await passwordToHash('abcdef'),
});
const request = {
@@ -127,6 +128,7 @@ describe('Testing mfa strategy', () => {
passwordValidForDays: 0,
passwordUpdatedAt: new Date(0),
userRoles: { isAdmin: true },
+ passwordHash: await passwordToHash('abcdef'),
});
const request = {
diff --git a/shared-helpers/src/auth/AuthContext.ts b/shared-helpers/src/auth/AuthContext.ts
index 0ef9379809..6bf148db37 100644
--- a/shared-helpers/src/auth/AuthContext.ts
+++ b/shared-helpers/src/auth/AuthContext.ts
@@ -52,7 +52,8 @@ type ContextProps = {
email: string,
password: string,
mfaCode?: string,
- mfaType?: MfaType
+ mfaType?: MfaType,
+ forPartners?: boolean
) => Promise
resetPassword: (
token: string,
@@ -223,16 +224,25 @@ export const AuthProvider: FunctionComponent = ({ child
email,
password,
mfaCode: string | undefined = undefined,
- mfaType: MfaType | undefined = undefined
+ mfaType: MfaType | undefined = undefined,
+ forPartners: boolean | undefined = undefined
) => {
dispatch(startLoading())
try {
const response = await authService?.login({ body: { email, password, mfaCode, mfaType } })
if (response) {
const profile = await userService?.profile()
- if (profile) {
+ if (
+ profile &&
+ (!forPartners ||
+ profile.userRoles?.isAdmin ||
+ profile.userRoles?.isJurisdictionalAdmin ||
+ profile.userRoles?.isPartner)
+ ) {
dispatch(saveProfile(profile))
return profile
+ } else {
+ throw Error("User cannot log in")
}
}
return undefined
diff --git a/shared-helpers/src/auth/catchNetworkError.ts b/shared-helpers/src/auth/catchNetworkError.ts
index e4c9cd64d6..e61114a421 100644
--- a/shared-helpers/src/auth/catchNetworkError.ts
+++ b/shared-helpers/src/auth/catchNetworkError.ts
@@ -38,7 +38,7 @@ export const useCatchNetworkError = () => {
const [networkError, setNetworkError] = useState(null)
const check401Error = (message: string, error: AxiosError) => {
- if (message.includes(NetworkErrorMessage.PasswordOutdated)) {
+ if (message?.includes(NetworkErrorMessage.PasswordOutdated)) {
setNetworkError({
title: t("authentication.signIn.passwordOutdated"),
description: `${t(
diff --git a/sites/partners/src/lib/users/signInHelpers.ts b/sites/partners/src/lib/users/signInHelpers.ts
index 5f46f6a619..e0844969f8 100644
--- a/sites/partners/src/lib/users/signInHelpers.ts
+++ b/sites/partners/src/lib/users/signInHelpers.ts
@@ -12,7 +12,7 @@ export const onSubmitEmailAndPassword =
async (data: { email: string; password: string }) => {
const { email, password } = data
try {
- await login(email, password)
+ await login(email, password, undefined, undefined, true)
await router.push("/")
} catch (error) {
if (error?.response?.data?.name === "mfaCodeIsMissing") {
@@ -86,7 +86,7 @@ export const onSubmitMfaCode =
async (data: { mfaCode: string }) => {
const { mfaCode } = data
try {
- await login(email, password, mfaCode, mfaType)
+ await login(email, password, mfaCode, mfaType, true)
resetNetworkError()
await router.push("/")
} catch (error) {
diff --git a/sites/partners/src/pages/sign-in.tsx b/sites/partners/src/pages/sign-in.tsx
index aff1ce8b0b..267d88ee64 100644
--- a/sites/partners/src/pages/sign-in.tsx
+++ b/sites/partners/src/pages/sign-in.tsx
@@ -93,7 +93,7 @@ const SignIn = () => {
)
useEffect(() => {
- if (networkError?.error.response.data?.message === "accountConfirmed") {
+ if (networkError?.error.response?.data?.message === "accountConfirmed") {
setConfirmationStatusModal(true)
}
}, [networkError])
diff --git a/sites/public/src/pages/sign-in.tsx b/sites/public/src/pages/sign-in.tsx
index ba148406df..55f9676faf 100644
--- a/sites/public/src/pages/sign-in.tsx
+++ b/sites/public/src/pages/sign-in.tsx
@@ -122,7 +122,7 @@ const SignIn = () => {
})()
useEffect(() => {
- if (networkError?.error?.response?.data?.message === "accountNotConfirmed") {
+ if (networkError?.error?.response?.data?.message?.includes("but is not confirmed")) {
setConfirmationStatusModal(true)
}
}, [networkError])