Skip to content

Commit

Permalink
feat: release 2023-05-02 (#612)
Browse files Browse the repository at this point in the history
* fix: uptake latest uic Modal, ActionBlock, FormCard breaking changes (bloom-housing#3358)

* feat: upgrade react to 18 (bloom-housing#3360)

* feat: upgrade nextjs to 13 (bloom-housing#3375)

* feat: upgrade nextjs to 13

* fix: attempt to get cypress test working

* feat: changing auth over to cookies (bloom-housing#3357)

* fix: resolves issues around markedAsDuplicate (bloom-housing#3373)

* fix: react type errors (bloom-housing#3382)

* refactor: add cloudinary fxn to partners (bloom-housing#3393)

* refactor: uptake seeds FormErrorMessage (bloom-housing#3369)

* fix: add startDate to open house submit event (bloom-housing#3399)

* fix: add three new fields to base view (bloom-housing#3406)

* feat: removing sensative info from leasing agent (bloom-housing#3409)

* feat: removing sensative info from leasing agent

* fix: adding swagger changes

* fix: updates for tests

* chore(deps): bump cookiejar from 2.1.2 to 2.1.4 (bloom-housing#3295)

Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.2 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

---
updated-dependencies:
- dependency-name: cookiejar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: updates around cookies (bloom-housing#3405)

* fix: updates around cookies

* fix: creating new migration for token -> code

* fix: Searching on the applications table causes the page to crash (bloom-housing#3408)

* fix: pass proper value to to_tsquery function

* fix: search applications using ILIKE

* fix: change where to orWhere

* feat: add application search by confirmation code

* updates proxy to support access control allow list (bloom-housing#3407)

* feat: updates proxy to support access control allow list

* fix: remove downstream access-control-allow-origin

* fix: update readme for m1

* fix: move purge call to the backend

* fix: test fix and add await

* fix: moving cache purge to helper

---------

Co-authored-by: Morgan Ludtke <ludtkemorgan@gmail.com>
Co-authored-by: Yazeed Loonat <yazeedloonat@gmail.com>

* fix: escape quote in translation update

* fix: add translation for 64 characters error (bloom-housing#3423)

* fix: downgrade version of nest axios (bloom-housing#3419)

* fix: now removes criteria file if a url is input (bloom-housing#3421)

* fix: remove check in test not applicable for hba

* fix: update ui-c to latest version (bloom-housing#3420)

* fix: update application test

* feat: 3291/listing export take 2 (bloom-housing#3424)

* fix: functional frontend

* fix: hooks and service updates

* fix: hitting service file

* fix: wip config work

* fix: wip config 2

* fix: completed query updates

* fix: complete column naming and basic formatting

* fix: clean up formatting

* fix: wip testing debugging

* fix: functional unit tests

* fix: cypress tests + formatting

* fix: unadded test changes

* fix: internal csv testing

* fix: exporter test fix

* fix: more detailed csv checks

* fix: testing + formatting tweaks

* fix: exporter testing improvements

* fix: updates per pr feedback

* fix: match config pattern

* fix: add close status option

* fix: reset netlify testing

* fix: final cleanup

* fix: rent type formatting

* fix: remove console log

* fix: missing state data (bloom-housing#3450)

* feat: adding knownError flag

* feat: adding knownError flag

* fix: partners highlight field on backend error (bloom-housing#3448)

* fix: partners highlight field on backend error

* fix: community type and disableUnitsAccordion fix

* fix: unit type fix for partial units

* fix: review comment addressed

* fix: phone number fix

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Yazeed Loonat <YazeedLoonat@gmail.com>
Co-authored-by: Emily Jablonski <65367387+emilyjablonski@users.noreply.github.com>
Co-authored-by: Krzysztof Zięcina <kziecina@airnauts.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sean Albert <smabert@gmail.com>
Co-authored-by: ColinBuyck <53269332+ColinBuyck@users.noreply.github.com>
  • Loading branch information
7 people committed May 18, 2023
1 parent f054a4e commit 504d260
Show file tree
Hide file tree
Showing 200 changed files with 7,375 additions and 6,398 deletions.
4 changes: 3 additions & 1 deletion backend/core/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ APP_SECRET='SOME-LONG-SECRET-KEY'
CLOUDINARY_SECRET=CLOUDINARY_SECRET
CLOUDINARY_KEY=CLOUDINARY_KEY
PARTNERS_BASE_URL=http://localhost:3001
PROXY_URL=
NEW_RELIC_APP_NAME=Bloom Backend Local
NEW_RELIC_LICENSE_KEY=
GOOGLE_API_ID=
GOOGLE_API_KEY=
GOOGLE_API_EMAIL=
PARTNERS_PORTAL_URL=http://localhost:3001
AFS_PROCESSING_CRON_STRING=0 * * * *
CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001"]
AFS_PROCESSING_CRON_STRING=0 * * * *
20 changes: 12 additions & 8 deletions backend/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bloom-housing/backend-core",
"version": "7.7.1",
"version": "7.13.0",
"description": "Listings service reference implementation for the Bloom affordable housing system",
"author": "Sean Albert <sean.albert@exygy.com>",
"private": false,
Expand Down Expand Up @@ -40,6 +40,7 @@
"dependencies": {
"@anchan828/nest-sendgrid": "^0.3.25",
"@google-cloud/translate": "^6.2.6",
"@nestjs/axios": "1.0.1",
"@nestjs/cli": "^8.2.1",
"@nestjs/common": "^8.3.1",
"@nestjs/config": "^1.2.0",
Expand All @@ -59,12 +60,14 @@
"class-transformer": "0.3.1",
"class-validator": "^0.14.0",
"cloudinary": "^1.25.2",
"cookie-parser": "1.4.6",
"dayjs": "^1.10.7",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"handlebars": "^4.7.6",
"joi": "^17.3.0",
"jwt-simple": "^0.5.6",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"nanoid": "^3.1.12",
"nestjs-twilio": "^2.1.0",
Expand All @@ -88,26 +91,27 @@
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/core": "^7.21.3",
"@babel/plugin-proposal-decorators": "^7.21.0",
"@nestjs/schematics": "^8.0.7",
"@nestjs/testing": "^8.3.1",
"@types/axios": "^0.14.0",
"@types/cron": "^1.7.3",
"@types/cookie-parser": "1.4.3",
"@types/express": "^4.17.8",
"@types/node": "^12.12.67",
"@types/passport-jwt": "^3.0.3",
"@types/passport-local": "^1.0.33",
"@types/supertest": "^2.0.10",
"@types/supertest": "2.0.12",
"dotenv": "^8.2.0",
"fishery": "^0.3.0",
"jest": "^26.5.3",
"supertest": "^4.0.2",
"jest": "^29.5.0",
"supertest": "6.2.4",
"swagger-axios-codegen": "0.11.16",
"ts-jest": "26.4.1",
"ts-jest": "^29.1.0",
"ts-loader": "^8.0.4",
"tsconfig-paths": "^3.9.0",
"yargs": "^16.0.3"
"yargs": "^17.7.2"
},
"jest": {
"moduleFileExtensions": [
Expand Down
6 changes: 2 additions & 4 deletions backend/core/scripts/import-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export async function importListing(
})

// Log in to retrieve an access token.
const { accessToken } = await authService.login({
await authService.login({
body: {
email: email,
password: password,
Expand All @@ -149,10 +149,8 @@ export async function importListing(
// Update the axios config so future requests include the access token in the header.
serviceOptions.axios = axios.create({
baseURL: apiUrl,
withCredentials: true,
timeout: 10000,
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
const unitTypes = await unitTypesService.list()
const priorityTypes = await unitAccessibilityPriorityTypesService.list()
Expand Down
12 changes: 11 additions & 1 deletion backend/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,17 @@ import { CatchAllFilter } from "./shared/filters/catch-all-filter"

export function applicationSetup(app: INestApplication) {
const { httpAdapter } = app.get(HttpAdapterHost)
app.enableCors()
const allowList = process.env.CORS_ORIGINS || []
app.enableCors((req, cb) => {
const options = {
credentials: true,
origin: false,
}
if (allowList.indexOf(req.header("Origin")) !== -1) {
options.origin = true
}
cb(null, options)
})
app.use(logger)
app.useGlobalFilters(new CatchAllFilter(httpAdapter))
app.use(bodyParser.json({ limit: "50mb" }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,21 @@ export class ApplicationFlaggedSetsService {
await transApplicationsRepository
.createQueryBuilder()
.update(Application)
.set({ reviewStatus: ApplicationReviewStatus.pendingAndValid })
.set({
reviewStatus: ApplicationReviewStatus.pendingAndValid,
markedAsDuplicate: false,
})
.where("id IN (:...selectedApps)", {
selectedApps,
})
.execute()
}

// mark those that were not selected as duplicate
// mark those that were not selected as pending
const qb = transApplicationsRepository
.createQueryBuilder()
.update(Application)
.set({ reviewStatus: ApplicationReviewStatus.pending })
.set({ reviewStatus: ApplicationReviewStatus.pending, markedAsDuplicate: false })
.where(
"exists (SELECT 1 FROM application_flagged_set_applications_applications WHERE applications_id = id AND application_flagged_set_id = :afsId)",
{ afsId: dto.afsId }
Expand All @@ -209,12 +212,12 @@ export class ApplicationFlaggedSetsService {
.where("id = :afsId", { afsId: dto.afsId })
.execute()
} else if (dto.status === FlaggedSetStatus.resolved) {
// mark selected a valid
// mark selected as valid
if (selectedApps.length) {
await transApplicationsRepository
.createQueryBuilder()
.update(Application)
.set({ reviewStatus: ApplicationReviewStatus.valid })
.set({ reviewStatus: ApplicationReviewStatus.valid, markedAsDuplicate: false })
.where("id IN (:...selectedApps)", {
selectedApps,
})
Expand All @@ -225,7 +228,7 @@ export class ApplicationFlaggedSetsService {
const qb = transApplicationsRepository
.createQueryBuilder()
.update(Application)
.set({ reviewStatus: ApplicationReviewStatus.duplicate })
.set({ reviewStatus: ApplicationReviewStatus.duplicate, markedAsDuplicate: true })
.where(
"exists (SELECT 1 FROM application_flagged_set_applications_applications WHERE applications_id = id AND application_flagged_set_id = :afsId)",
{ afsId: dto.afsId }
Expand Down
40 changes: 27 additions & 13 deletions backend/core/src/applications/services/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Scope,
} from "@nestjs/common"
import { InjectRepository } from "@nestjs/typeorm"
import { DeepPartial, QueryFailedError, Repository } from "typeorm"
import { Brackets, DeepPartial, QueryFailedError, Repository } from "typeorm"
import { paginate, Pagination, PaginationTypeEnum } from "nestjs-typeorm-paginate"
import { Request as ExpressRequest } from "express"
import { REQUEST } from "@nestjs/core"
Expand All @@ -27,7 +27,6 @@ import { ApplicationUpdateDto } from "../dto/application-update.dto"
import { ApplicationsCsvListQueryParams } from "../dto/applications-csv-list-query-params"
import { ListingRepository } from "../../listings/db/listing.repository"
import { Listing } from "../../listings/entities/listing.entity"
import { ApplicationReviewStatus } from "../types/application-review-status-enum"

@Injectable({ scope: Scope.REQUEST })
export class ApplicationsService {
Expand Down Expand Up @@ -109,11 +108,6 @@ export class ApplicationsService {
)
const flags = await Promise.all(promiseArray)
result.forEach((application, index) => {
if (flags[index].status) {
application.markedAsDuplicate = flags[index].status === ApplicationReviewStatus.duplicate
} else {
application.markedAsDuplicate = false
}
application.flagged = !!flags[index].id
})

Expand Down Expand Up @@ -272,13 +266,33 @@ export class ApplicationsService {
listingId: (qb, { listingId }) =>
qb.andWhere("application.listing_id = :lid", { lid: listingId }),
orderBy: (qb, { orderBy, order }) => qb.orderBy(orderBy, order, "NULLS LAST"),
search: (qb, { search }) =>
search: (qb, { search }) => {
qb.andWhere(
`to_tsvector('english', REGEXP_REPLACE(concat_ws(' ', applicant, alternateContact.emailAddress), '[_]|[-]', '/', 'g')) @@ to_tsquery(CONCAT(CAST(REGEXP_REPLACE(:search, '[_]|[-]', '/', 'g') as text), ':*'))`,
{
search,
}
),
new Brackets((subQb) => {
subQb.where("application.confirmationCode ILIKE :search", { search: `%${search}%` })
subQb.orWhere("applicant.firstName ILIKE :search", { search: `%${search}%` })
subQb.orWhere("applicant.lastName ILIKE :search", { search: `%${search}%` })
subQb.orWhere("applicant.emailAddress ILIKE :search", {
search: `%${search}%`,
})
subQb.orWhere("applicant.phoneNumber ILIKE :search", { search: `%${search}%` })
subQb.orWhere(
"CONCAT(applicant.firstName, ' ', applicant.lastName, ' ', applicant.emailAddress, ' ', applicant.phoneNumber) ILIKE :search",
{ search: `%${search}%` }
)
subQb.orWhere("alternateContact.firstName ILIKE :search", { search: `%${search}%` })
subQb.orWhere("alternateContact.lastName ILIKE :search", { search: `%${search}%` })
subQb.orWhere("alternateContact.emailAddress ILIKE :search", {
search: `%${search}%`,
})
subQb.orWhere("alternateContact.phoneNumber ILIKE :search", { search: `%${search}%` })
subQb.orWhere(
"CONCAT(alternateContact.firstName, ' ', alternateContact.lastName, ' ', alternateContact.emailAddress, ' ', alternateContact.phoneNumber) ILIKE :search",
{ search: `%${search}%` }
)
})
)
},
}

// --> Build main query
Expand Down
2 changes: 1 addition & 1 deletion backend/core/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { CsvBuilder } from "../applications/services/csv-builder.service"
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>("APP_SECRET"),
signOptions: {
expiresIn: "10m",
expiresIn: "1d",
},
}),
}),
Expand Down
24 changes: 24 additions & 0 deletions backend/core/src/auth/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
import { CookieOptions } from "express"
// Length of hashed key, in bytes
export const SCRYPT_KEYLEN = 64
export const SALT_SIZE = SCRYPT_KEYLEN
export const TOKEN_COOKIE_NAME = "access-token"
export const REFRESH_COOKIE_NAME = "refresh-token"
export const ACCESS_TOKEN_AVAILABLE_NAME = "access-token-available"
export const TOKEN_COOKIE_MAXAGE = 86400000 // 24 hours
export const AUTH_COOKIE_OPTIONS: CookieOptions = {
httpOnly: true,
// since our local env doesn't have an https cert we can't be secure. Hosted envs should be secure
secure: process.env.NODE_ENV !== "development",
sameSite: process.env.NODE_ENV === "development" ? "strict" : "none",
maxAge: TOKEN_COOKIE_MAXAGE / 24, // access token should last 1 hr
}
export const REFRESH_COOKIE_OPTIONS: CookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV !== "development",
sameSite: process.env.NODE_ENV === "development" ? "strict" : "none",
maxAge: TOKEN_COOKIE_MAXAGE,
}
export const ACCESS_TOKEN_AVAILABLE_OPTIONS: CookieOptions = {
httpOnly: false,
secure: process.env.NODE_ENV !== "development",
sameSite: process.env.NODE_ENV === "development" ? "strict" : "none",
maxAge: TOKEN_COOKIE_MAXAGE / 24, // flag should last 1 hr
}
53 changes: 41 additions & 12 deletions backend/core/src/auth/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import {
Controller,
Request,
Response,
Post,
UseGuards,
UsePipes,
ValidationPipe,
Body,
Get,
BadRequestException,
} from "@nestjs/common"
import { LocalMfaAuthGuard } from "../guards/local-mfa-auth.guard"
import { AuthService } from "../services/auth.service"
import { DefaultAuthGuard } from "../guards/default.guard"
import { ApiBody, ApiExtraModels, ApiOperation, ApiTags } from "@nestjs/swagger"
import { ApiBearerAuth, ApiBody, ApiExtraModels, ApiOperation, ApiTags } from "@nestjs/swagger"
import { LoginDto } from "../dto/login.dto"
import { mapTo } from "../../shared/mapTo"
import { defaultValidationPipeOptions } from "../../shared/default-validation-pipe-options"
import { LoginResponseDto } from "../dto/login-response.dto"
import { StatusDto } from "../../shared/dto/status.dto"
import { LogoutResponseDto } from "../dto/logout-response.dto"
import { RequestMfaCodeDto } from "../dto/request-mfa-code.dto"
import { RequestMfaCodeResponseDto } from "../dto/request-mfa-code-response.dto"
import { UserService } from "../services/user.service"
import { GetMfaInfoDto } from "../dto/get-mfa-info.dto"
import { GetMfaInfoResponseDto } from "../dto/get-mfa-info-response.dto"
import { UserErrorExtraModel } from "../user-errors"
import { TokenDto } from "../dto/token.dto"
import { REFRESH_COOKIE_NAME } from "../constants"
import { Response as ExpressResponse } from "express"
import { OptionalAuthGuard } from "../guards/optional-auth.guard"

@Controller("auth")
@ApiTags("auth")
Expand All @@ -37,18 +43,18 @@ export class AuthController {
@Post("login")
@ApiBody({ type: LoginDto })
@ApiOperation({ summary: "Login", operationId: "login" })
login(@Request() req): LoginResponseDto {
const accessToken = this.authService.generateAccessToken(req.user)
return mapTo(LoginResponseDto, { accessToken })
async login(
@Request() req,
@Response({ passthrough: true }) res: ExpressResponse
): Promise<StatusDto> {
return mapTo(StatusDto, await this.authService.tokenGen(res, req.user))
}

@UseGuards(DefaultAuthGuard)
@Post("token")
@ApiBody({ type: TokenDto })
@ApiOperation({ summary: "Token", operationId: "token" })
token(@Request() req): LoginResponseDto {
const accessToken = this.authService.generateAccessToken(req.user)
return mapTo(LoginResponseDto, { accessToken })
@Get("logout")
@ApiOperation({ summary: "Logout", operationId: "logout" })
async logout(@Request() req, @Response({ passthrough: true }) res): Promise<LogoutResponseDto> {
return mapTo(LogoutResponseDto, await this.authService.tokenClear(res, req.user))
}

@Post("request-mfa-code")
Expand All @@ -66,4 +72,27 @@ export class AuthController {
const getMfaInfoResponseDto = await this.userService.getMfaInfo(getMfaInfoDto)
return mapTo(GetMfaInfoResponseDto, getMfaInfoResponseDto)
}

@Get("requestNewToken")
@ApiOperation({
summary: "Requests a new token given a refresh token",
operationId: "requestNewToken",
})
@ApiBearerAuth()
@UseGuards(OptionalAuthGuard)
async requestNewToken(
@Request() req,
@Response({ passthrough: true }) res: ExpressResponse
): Promise<StatusDto> {
if (!req?.cookies[REFRESH_COOKIE_NAME]) {
throw new BadRequestException({
message: "No refresh token sent with request",
knownError: true,
})
}
return mapTo(
StatusDto,
await this.authService.tokenGen(res, req.user, req.cookies[REFRESH_COOKIE_NAME])
)
}
}
Loading

0 comments on commit 504d260

Please sign in to comment.