Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Jurisdiction to listings, users, translations (replaces countyCode) #1776

Merged
merged 31 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ff0c71b
force quit redis connection on app close
seanmalbert Aug 11, 2021
72e1fc4
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 11, 2021
9fbdfb6
updates for redis config
seanmalbert Aug 11, 2021
7536522
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 11, 2021
7007eb3
adds enableShutdownHooks to main app
seanmalbert Aug 11, 2021
729f024
merge upstream
seanmalbert Aug 16, 2021
092ffd1
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 16, 2021
d831039
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 19, 2021
f133c3e
Update archer-listing.ts
seanmalbert Aug 19, 2021
e1cd154
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 20, 2021
7d756d7
Updates cache clear to use cacheManager reset, so we don't have to ma…
seanmalbert Aug 20, 2021
348ef97
Update units-transformations.ts
seanmalbert Aug 20, 2021
2fc468a
Update CHANGELOG.md
seanmalbert Aug 20, 2021
5b49201
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 20, 2021
09ad286
allows for not needing google api keys
seanmalbert Aug 20, 2021
173e05c
Update listings.controller.ts
seanmalbert Aug 20, 2021
af6db79
Update listings.e2e-spec.ts
seanmalbert Aug 20, 2021
5f3d9b7
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 20, 2021
a20d273
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 25, 2021
8cb1e60
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 25, 2021
b5dfa0b
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 27, 2021
4ace68e
Merge remote-tracking branch 'remotes/upstream/dev' into origin/dev
seanmalbert Aug 29, 2021
2aef896
adds jurisdiction relations
seanmalbert Aug 29, 2021
d15aabc
cleanup after testing
seanmalbert Aug 30, 2021
92d1978
adds jurisdiction selection to listings form
seanmalbert Aug 30, 2021
39b9a61
updates backend tests
seanmalbert Aug 31, 2021
21a6373
updates to ui-components
seanmalbert Aug 31, 2021
96f32ab
testing cypress tests
seanmalbert Aug 31, 2021
66349f2
renamed JuriWrap to JurisdictionWrapper
seanmalbert Aug 31, 2021
b2a680c
addresses feedback from review
seanmalbert Sep 1, 2021
44cee18
Adds changelog entry
seanmalbert Sep 3, 2021
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ All notable changes to this project will be documented in this file. The format
- Adds `roles` property to `UserDto [#1575](https://github.com/bloom-housing/bloom/pull/1575)
- Adds UnitAmiChartOverride entity and implements ami chart overriding at Unit level [#1575](https://github.com/bloom-housing/bloom/pull/1575)
- Adds `authz.e2e-spec.ts` test cover for preventing user from voluntarily changing his associated `roles` object [#1575](https://github.com/bloom-housing/bloom/pull/1575)
- Adds Jurisdictions to users, listings and translations. The migration script assigns the first alpha sorted jurisdiction to users, so this piece may need to be changed for Detroit, if they have more than Detroit in their DB. [#1776](https://github.com/bloom-housing/bloom/pull/1776)

- Changed:

Expand Down
7 changes: 6 additions & 1 deletion backend/core/archer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
AmiChart,
CountyCode,
CSVFormattingType,
Listing,
ListingReviewOrder,
ListingStatus,
UnitStatus,
} from "./types"

import { CountyCode } from "./src/shared/types/county-code"

export const SanMateoHUD2019: AmiChart = {
id: "ami_chart_id",
createdAt: new Date(),
Expand Down Expand Up @@ -253,6 +254,10 @@ export const ArcherListing: Listing = {
applicationDropOffAddressOfficeHours: null,
applicationMailingAddress: null,
countyCode: CountyCode["San Jose"],
jurisdiction: {
id: "id",
name: "San Jose",
},
depositMax: "",
disableUnitsAccordion: false,
events: [],
Expand Down
4 changes: 3 additions & 1 deletion backend/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ try {
} catch {
// Pass
}
import "newrelic"
if (process.env.NEW_RELIC_APP_NAME && process.env.NEW_RELIC_LICENSE_KEY) {
require("newrelic")
}
import { ClassSerializerInterceptor, DynamicModule, INestApplication, Module } from "@nestjs/common"
import { TypeOrmModule } from "@nestjs/typeorm"
// Use require because of the CommonJS/AMD style export.
Expand Down
2 changes: 2 additions & 0 deletions backend/core/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { UserService } from "./services/user.service"
import { UserController } from "./controllers/user.controller"
import { EmailModule } from "../shared/email/email.module"
import { PasswordService } from "./services/password.service"
import { JurisdictionsModule } from "../jurisdictions/jurisdictions.module"

@Module({
imports: [
Expand All @@ -31,6 +32,7 @@ import { PasswordService } from "./services/password.service"
}),
TypeOrmModule.forFeature([RevokedToken, User]),
SharedModule,
JurisdictionsModule,
EmailModule,
],
providers: [LocalStrategy, JwtStrategy, AuthService, AuthzService, UserService, PasswordService],
Expand Down
30 changes: 30 additions & 0 deletions backend/core/src/auth/dto/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@ import {
} from "class-validator"
import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum"
import { IdNameDto } from "../../shared/dto/idName.dto"
import { IdDto } from "../../shared/dto/id.dto"
import { Match } from "../../shared/decorators/match.decorator"
import { passwordRegex } from "../../shared/password-regex"
import { PaginationFactory, PaginationAllowsAllQueryParams } from "../../shared/dto/pagination.dto"
import { UserRolesDto } from "./user-roles.dto"
import { JurisdictionDto } from "../../jurisdictions/dto/jurisdiction.dto"

export class UserDto extends OmitType(User, [
"leasingAgentInListings",
"passwordHash",
"resetToken",
"confirmationToken",
"roles",
"jurisdictions",
] as const) {
@Expose()
@IsOptional()
Expand All @@ -40,6 +43,12 @@ export class UserDto extends OmitType(User, [
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@Type(() => UserRolesDto)
roles?: UserRolesDto | null

@Expose()
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => JurisdictionDto)
jurisdictions: JurisdictionDto[]
}

export class UserBasicDto extends OmitType(User, [
Expand All @@ -48,12 +57,19 @@ export class UserBasicDto extends OmitType(User, [
"confirmationToken",
"resetToken",
"roles",
"jurisdictions",
] as const) {
@Expose()
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => UserRolesDto)
roles: UserRolesDto

@Expose()
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => JurisdictionDto)
jurisdictions: JurisdictionDto[]
}

export class UserDtoWithAccessToken extends UserDto {
Expand All @@ -79,6 +95,7 @@ export class UserCreateDto extends OmitType(UserDto, [
"updatedAt",
"leasingAgentInListings",
"roles",
"jurisdictions",
] as const) {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
Expand All @@ -104,6 +121,12 @@ export class UserCreateDto extends OmitType(UserDto, [
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MaxLength(256, { groups: [ValidationsGroupsEnum.default] })
appUrl?: string | null

@Expose()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => JurisdictionDto)
jurisdictions?: JurisdictionDto[]
}

export class UserUpdateDto extends OmitType(UserDto, [
Expand All @@ -112,6 +135,7 @@ export class UserUpdateDto extends OmitType(UserDto, [
"updatedAt",
"leasingAgentInListings",
"roles",
"jurisdictions",
] as const) {
@Expose()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
Expand Down Expand Up @@ -143,6 +167,12 @@ export class UserUpdateDto extends OmitType(UserDto, [
@ValidateIf((o) => o.password, { groups: [ValidationsGroupsEnum.default] })
@IsNotEmpty({ groups: [ValidationsGroupsEnum.default] })
currentPassword?: string

@Expose()
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => IdDto)
jurisdictions: IdDto[]
}

export class UserListQueryParams extends PaginationAllowsAllQueryParams {}
Expand Down
6 changes: 6 additions & 0 deletions backend/core/src/auth/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
CreateDateColumn,
Entity,
Index,
JoinTable,
ManyToMany,
OneToOne,
PrimaryGeneratedColumn,
Expand All @@ -16,6 +17,7 @@ import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enu
import { ApiProperty } from "@nestjs/swagger"
import { Language } from "../../shared/types/language-enum"
import { UserRoles } from "./user-roles.entity"
import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity"

@Entity({ name: "user_accounts" })
@Unique(["email"])
Expand Down Expand Up @@ -101,4 +103,8 @@ export class User {
@IsEnum(Language, { groups: [ValidationsGroupsEnum.default] })
@ApiProperty({ enum: Language, enumName: "Language" })
language?: Language | null

@ManyToMany(() => Jurisdiction, { cascade: true, eager: true })
@JoinTable()
jurisdictions: Jurisdiction[]
}
7 changes: 7 additions & 0 deletions backend/core/src/auth/services/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EmailService } from "../../shared/email/email.service"
import { AuthService } from "./auth.service"
import { AuthzService } from "./authz.service"
import { PasswordService } from "./password.service"
import { JurisdictionResolverService } from "../../jurisdictions/services/jurisdiction-resolver.service"

// Cypress brings in Chai types for the global expect, but we want to use jest
// expect here so we need to re-declare it.
Expand Down Expand Up @@ -37,6 +38,12 @@ describe("UserService", () => {
provide: AuthService,
useValue: { generateAccessToken: jest.fn().mockReturnValue("accessToken") },
},
{
provide: JurisdictionResolverService,
useValue: {
getJurisdiction: jest.fn(),
},
},
AuthzService,
PasswordService,
],
Expand Down
23 changes: 21 additions & 2 deletions backend/core/src/auth/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ForgotPasswordDto } from "../dto/forgot-password.dto"

import { AuthContext } from "../types/auth-context"
import { PasswordService } from "./password.service"
import { JurisdictionResolverService } from "../../jurisdictions/services/jurisdiction-resolver.service"

@Injectable({ scope: Scope.REQUEST })
export class UserService {
Expand All @@ -32,7 +33,8 @@ export class UserService {
private readonly emailService: EmailService,
private readonly authService: AuthService,
private readonly authzService: AuthzService,
private readonly passwordService: PasswordService
private readonly passwordService: PasswordService,
private readonly jurisdictionResolverService: JurisdictionResolverService
) {}

public async findByEmail(email: string) {
Expand Down Expand Up @@ -100,6 +102,20 @@ export class UserService {
delete dto.password
}

/**
* jurisdictions should be filtered based off of what the authContext user has
*/
if (authContext.user.jurisdictions) {
if (dto.jurisdictions) {
dto.jurisdictions = dto.jurisdictions.filter(
(jurisdiction) =>
authContext.user.jurisdictions.findIndex((val) => val.id === jurisdiction.id) > -1
)
}
} else {
delete dto.jurisdictions
}

assignDefined(user, {
...dto,
passwordHash,
Expand Down Expand Up @@ -159,7 +175,6 @@ export class UserService {
...dto,
})
}

let user = await this.findByEmail(dto.email)
if (user) {
throw new HttpException(USER_ERRORS.EMAIL_IN_USE.message, USER_ERRORS.EMAIL_IN_USE.status)
Expand All @@ -172,6 +187,10 @@ export class UserService {
user.dob = dto.dob
user.email = dto.email
user.language = dto.language
// if coming from partners dto.jurisdictions can be set
user.jurisdictions = dto.jurisdictions
Copy link
Contributor

Choose a reason for hiding this comment

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

@seanmalbert Could you explain a bit more the reasoning behind this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@pbn4 , As part of the user management flow, a user can belong to more than one jurisdiction and, given they have permissions, when they create a user they will have to select which jurisdiction they belong to. Therefore it would be set on the incoming dto, not from the request header (which partner's doesn't currently use).

? dto.jurisdictions
: [await this.jurisdictionResolverService.getJurisdiction()]
try {
user.passwordHash = await this.passwordService.passwordToHash(password)
user = await this.repo.save(user)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enu

@Entity({ name: "jurisdictions" })
export class Jurisdiction extends AbstractEntity {
@Column({ type: "text" })
@Column({ type: "text", unique: true })
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
@MaxLength(256, { groups: [ValidationsGroupsEnum.default] })
Expand Down
2 changes: 1 addition & 1 deletion backend/core/src/jurisdictions/jurisdictions.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { AuthzGuard } from "../auth/guards/authz.guard"
import { ResourceType } from "../auth/decorators/resource-type.decorator"
import { mapTo } from "../shared/mapTo"
import { defaultValidationPipeOptions } from "../shared/default-validation-pipe-options"
import { JurisdictionsService } from "./jurisdictions.service"
import { JurisdictionsService } from "./services/jurisdictions.service"
import {
JurisdictionCreateDto,
JurisdictionDto,
Expand Down
10 changes: 6 additions & 4 deletions backend/core/src/jurisdictions/jurisdictions.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Module } from "@nestjs/common"
import { forwardRef, Module } from "@nestjs/common"
import { TypeOrmModule } from "@nestjs/typeorm"
import { AuthModule } from "../auth/auth.module"
import { Jurisdiction } from "./entities/jurisdiction.entity"
import { JurisdictionsController } from "./jurisdictions.controller"
import { JurisdictionsService } from "./jurisdictions.service"
import { JurisdictionsService } from "./services/jurisdictions.service"
import { JurisdictionResolverService } from "./services/jurisdiction-resolver.service"

@Module({
imports: [TypeOrmModule.forFeature([Jurisdiction]), AuthModule],
imports: [TypeOrmModule.forFeature([Jurisdiction]), forwardRef(() => AuthModule)],
controllers: [JurisdictionsController],
providers: [JurisdictionsService],
providers: [JurisdictionsService, JurisdictionResolverService],
exports: [JurisdictionResolverService],
})
export class JurisdictionsModule {}
9 changes: 0 additions & 9 deletions backend/core/src/jurisdictions/jurisdictions.service.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Inject, Injectable, NotFoundException, Scope } from "@nestjs/common"
import { REQUEST } from "@nestjs/core"
import { InjectRepository } from "@nestjs/typeorm"
import { Request as ExpressRequest } from "express"
import { Repository } from "typeorm"
import { Jurisdiction } from "../entities/jurisdiction.entity"

@Injectable({ scope: Scope.REQUEST })
export class JurisdictionResolverService {
constructor(
@Inject(REQUEST) private req: ExpressRequest,
@InjectRepository(Jurisdiction)
private readonly jurisdictionRepository: Repository<Jurisdiction>
) {}

async getJurisdiction(): Promise<Jurisdiction> {
const jurisdictionName = this.req.get("jurisdictionName")
const jurisdiction = await this.jurisdictionRepository.findOne({
where: { name: jurisdictionName },
})

if (jurisdiction === undefined) {
throw new NotFoundException("The jurisdiction is not configured.")
}

return jurisdiction
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AbstractServiceFactory } from "../../shared/services/abstract-service"
import { Jurisdiction } from "../entities/jurisdiction.entity"
import { JurisdictionCreateDto, JurisdictionUpdateDto } from "../dto/jurisdiction.dto"

export class JurisdictionsService extends AbstractServiceFactory<
Jurisdiction,
JurisdictionCreateDto,
JurisdictionUpdateDto
>(Jurisdiction) {}
Loading