Skip to content

Commit

Permalink
fix: jurisdiction permission array approach (#3644)
Browse files Browse the repository at this point in the history
* fix: jurisdiction permission array approach

* fix: corrected listing tests

* fix: complete existing test coverage

* fix: wip listing serving testing

* fix: functioning service tests

* fix: basic create and update e2e

* fix: e2e debug logging

* fix: temp testing fix

* fix: remote test fix

* fix: functioning e2e testing

* fix: increase testing consistency

* fix: attempt 2 resolving test inconsistencies

* fix: email object contains approach

* fix: i believe

* fix: i really believe

* fix: cover juris configuration case

* fix: remove console logs

* fix: update commenting

* fix: additional testing checks
  • Loading branch information
ColinBuyck authored Sep 21, 2023
1 parent 40a957c commit b45211e
Show file tree
Hide file tree
Showing 15 changed files with 1,183 additions and 186 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enu
import { Language } from "../../shared/types/language-enum"
import { Expose, Type } from "class-transformer"
import { MultiselectQuestion } from "../../multiselect-question/entities/multiselect-question.entity"
import { UserRoleEnum } from "../../../src/auth/enum/user-role-enum"

@Entity({ name: "jurisdictions" })
export class Jurisdiction extends AbstractEntity {
Expand All @@ -36,6 +37,14 @@ export class Jurisdiction extends AbstractEntity {
@IsEnum(Language, { groups: [ValidationsGroupsEnum.default], each: true })
languages: Language[]

@Column({ type: "enum", enum: UserRoleEnum, array: true, nullable: true })
@Expose()
@IsArray({ groups: [ValidationsGroupsEnum.default] })
@ArrayMaxSize(256, { groups: [ValidationsGroupsEnum.default] })
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsEnum(UserRoleEnum, { groups: [ValidationsGroupsEnum.default], each: true })
listingApprovalPermissions?: UserRoleEnum[]

@ManyToMany(
() => MultiselectQuestion,
(multiselectQuestion) => multiselectQuestion.jurisdictions,
Expand Down
19 changes: 2 additions & 17 deletions backend/core/src/listings/listings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export class ListingsController {
@Post()
@ApiOperation({ summary: "Create listing", operationId: "create" })
@UsePipes(new ListingCreateValidationPipe(defaultValidationPipeOptions))
async create(@Body() listingDto: ListingCreateDto): Promise<ListingDto> {
const listing = await this.listingsService.create(listingDto)
async create(@Request() req, @Body() listingDto: ListingCreateDto): Promise<ListingDto> {
const listing = await this.listingsService.create(listingDto, req.user)
return mapTo(ListingDto, listing)
}

Expand Down Expand Up @@ -121,21 +121,6 @@ export class ListingsController {
return mapTo(ListingDto, listing)
}

@Put(`updateAndNotify/:id`)
@ApiOperation({
summary: "Update listing by id and notify relevant users",
operationId: "updateAndNotify",
})
@UsePipes(new ListingUpdateValidationPipe(defaultValidationPipeOptions))
async updateAndNotify(
@Request() req,
@Param("id") listingId: string,
@Body() listingUpdateDto: ListingUpdateDto
): Promise<ListingDto> {
const listing = await this.listingsService.updateAndNotify(listingUpdateDto, req.user)
return mapTo(ListingDto, listing)
}

@Delete()
@ApiOperation({ summary: "Delete listing by id", operationId: "delete" })
@UsePipes(new ValidationPipe(defaultValidationPipeOptions))
Expand Down
211 changes: 123 additions & 88 deletions backend/core/src/listings/listings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Brackets, In, Repository } from "typeorm"
import { Listing } from "./entities/listing.entity"
import { getView } from "./views/view"
import { summarizeUnits, summarizeUnitsByTypeAndRent } from "../shared/units-transformations"
import { Language, ListingReviewOrder } from "../../types"
import { IdName, Language, ListingReviewOrder } from "../../types"
import { AmiChart } from "../ami-charts/entities/ami-chart.entity"
import { ListingCreateDto } from "./dto/listing-create.dto"
import { ListingUpdateDto } from "./dto/listing-update.dto"
Expand All @@ -21,7 +21,9 @@ import { ApplicationFlaggedSetsService } from "../application-flagged-sets/appli
import { ListingsQueryBuilder } from "./db/listing-query-builder"
import { CachePurgeService } from "./cache-purge.service"
import { EmailService } from "../email/email.service"
import { JurisdictionsService } from "../jurisdictions/services/jurisdictions.service"
import { ConfigService } from "@nestjs/config"
import { UserRoleEnum } from "../../src/auth/enum/user-role-enum"

@Injectable({ scope: Scope.REQUEST })
export class ListingsService {
Expand All @@ -35,6 +37,7 @@ export class ListingsService {
private readonly afsService: ApplicationFlaggedSetsService,
private readonly cachePurgeService: CachePurgeService,
private readonly emailService: EmailService,
private readonly jurisdictionsService: JurisdictionsService,
private readonly configService: ConfigService
) {}

Expand Down Expand Up @@ -98,7 +101,7 @@ export class ListingsService {
}
}

async create(listingDto: ListingCreateDto) {
async create(listingDto: ListingCreateDto, user: User) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
await this.authzService.canOrThrow(this.req.user as User, "listing", authzActions.create, {
jurisdictionId: listingDto.jurisdiction.id,
Expand All @@ -109,8 +112,23 @@ export class ListingsService {
publishedAt: listingDto.status === ListingStatus.active ? new Date() : null,
closedAt: listingDto.status === ListingStatus.closed ? new Date() : null,
})

return await listing.save()
const saveResponse = await listing.save()
// only listings approval state possible from creation
if (listing.status === ListingStatus.pendingReview) {
const listingApprovalPermissions = (
await this.jurisdictionsService.findOne({
where: { id: saveResponse.jurisdiction.id },
})
)?.listingApprovalPermissions
await this.listingApprovalNotify({
user,
listingInfo: { id: saveResponse.id, name: saveResponse.name },
status: listing.status,
approvingRoles: listingApprovalPermissions,
jurisId: listing.jurisdiction.id,
})
}
return saveResponse
}

async update(listingDto: ListingUpdateDto, user: User) {
Expand Down Expand Up @@ -168,6 +186,21 @@ export class ListingsService {
})

const saveResponse = await this.listingRepository.save(listing)
const listingApprovalPermissions = (
await this.jurisdictionsService.findOne({
where: { id: listing.jurisdiction.id },
})
)?.listingApprovalPermissions

if (listingApprovalPermissions?.length > 0)
await this.listingApprovalNotify({
user,
listingInfo: { id: listing.id, name: listing.name },
approvingRoles: listingApprovalPermissions,
status: listing.status,
previousStatus,
jurisId: listing.jurisdiction.id,
})
await this.cachePurgeService.cachePurgeForSingleListing(previousStatus, newStatus, saveResponse)
return saveResponse
}
Expand Down Expand Up @@ -216,120 +249,122 @@ export class ListingsService {
return listing.jurisdiction.id
}

public async getApprovingUserEmails(): Promise<string[]> {
const approvingUsers = await this.userRepository
.createQueryBuilder("user")
.select(["user.email"])
.leftJoin("user.roles", "userRoles")
.where("userRoles.is_admin = :is_admin", {
is_admin: true,
})
.getMany()
const approvingUserEmails: string[] = []
approvingUsers?.forEach((user) => user?.email && approvingUserEmails.push(user.email))
return approvingUserEmails
}

public async getNonApprovingUserInfo(
listingId: string,
jurisId: string,
public async getUserEmailInfo(
userRoles: UserRoleEnum | UserRoleEnum[],
listingId?: string,
jurisId?: string,
getPublicUrl = false
): Promise<{ emails: string[]; publicUrl?: string | null }> {
//determine select statement
const selectFields = ["user.email", "jurisdictions.id"]
getPublicUrl && selectFields.push("jurisdictions.publicUrl")
const nonApprovingUsers = await this.userRepository

//build potential where statements
const admin = new Brackets((qb) => {
qb.where("userRoles.is_admin = :is_admin", {
is_admin: true,
})
})
const jurisdictionAdmin = new Brackets((qb) => {
qb.where("userRoles.is_jurisdictional_admin = :is_jurisdictional_admin", {
is_jurisdictional_admin: true,
}).andWhere("jurisdictions.id = :jurisId", {
jurisId: jurisId,
})
})
const partner = new Brackets((qb) => {
qb.where("userRoles.is_partner = :is_partner", {
is_partner: true,
}).andWhere("leasingAgentInListings.id = :listingId", {
listingId: listingId,
})
})

let userQueryBuilder = this.userRepository
.createQueryBuilder("user")
.select(selectFields)
.leftJoin("user.leasingAgentInListings", "leasingAgentInListings")
.leftJoin("user.roles", "userRoles")
.leftJoin("user.jurisdictions", "jurisdictions")
.where(
new Brackets((qb) => {
qb.where("userRoles.is_partner = :is_partner", {
is_partner: true,
}).andWhere("leasingAgentInListings.id = :listingId", {
listingId: listingId,
})
})
)
.orWhere(
new Brackets((qb) => {
qb.where("userRoles.is_jurisdictional_admin = :is_jurisdictional_admin", {
is_jurisdictional_admin: true,
}).andWhere("jurisdictions.id = :jurisId", {
jurisId: jurisId,
})
})
)
.getMany()

// determine where clause(s)
if (userRoles.includes(UserRoleEnum.admin)) userQueryBuilder = userQueryBuilder.where(admin)
if (userRoles.includes(UserRoleEnum.partner)) userQueryBuilder = userQueryBuilder.where(partner)
if (userRoles.includes(UserRoleEnum.jurisdictionAdmin)) {
userQueryBuilder = userQueryBuilder.orWhere(jurisdictionAdmin)
}

const userResults = await userQueryBuilder.getMany()

// account for users having access to multiple jurisdictions
const publicUrl = getPublicUrl
? nonApprovingUsers[0]?.jurisdictions?.find((juris) => juris.id === jurisId)?.publicUrl
? userResults[0]?.jurisdictions?.find((juris) => juris.id === jurisId)?.publicUrl
: null
const nonApprovingUserEmails: string[] = []
nonApprovingUsers?.forEach((user) => user?.email && nonApprovingUserEmails.push(user.email))
return { emails: nonApprovingUserEmails, publicUrl }
const userEmails: string[] = []
userResults?.forEach((user) => user?.email && userEmails.push(user.email))
return { emails: userEmails, publicUrl }
}

async updateAndNotify(listingData: ListingUpdateDto, user: User) {
let result
// partners updates status to pending review when requesting admin approval
if (listingData.status === ListingStatus.pendingReview) {
result = await this.update(listingData, user)
const approvingUserEmails = await this.getApprovingUserEmails()
public async listingApprovalNotify(params: {
user: User
listingInfo: IdName
status: ListingStatus
approvingRoles: UserRoleEnum[]
previousStatus?: ListingStatus
jurisId?: string
}) {
const nonApprovingRoles = [UserRoleEnum.partner]
if (!params.approvingRoles.includes(UserRoleEnum.jurisdictionAdmin))
nonApprovingRoles.push(UserRoleEnum.jurisdictionAdmin)
if (params.status === ListingStatus.pendingReview) {
const userInfo = await this.getUserEmailInfo(
params.approvingRoles,
params.listingInfo.id,
params.jurisId
)
await this.emailService.requestApproval(
user,
{ id: listingData.id, name: listingData.name },
approvingUserEmails,
params.user,
{ id: params.listingInfo.id, name: params.listingInfo.name },
userInfo.emails,
this.configService.get("PARTNERS_PORTAL_URL")
)
}
// admin updates status to changes requested when approval requires partner changes
else if (listingData.status === ListingStatus.changesRequested) {
result = await this.update(listingData, user)
const nonApprovingUserInfo = await this.getNonApprovingUserInfo(
listingData.id,
listingData.jurisdiction.id
else if (params.status === ListingStatus.changesRequested) {
const userInfo = await this.getUserEmailInfo(
nonApprovingRoles,
params.listingInfo.id,
params.jurisId
)
await this.emailService.changesRequested(
user,
{ id: listingData.id, name: listingData.name },
nonApprovingUserInfo.emails,
params.user,
{ id: params.listingInfo.id, name: params.listingInfo.name },
userInfo.emails,
this.configService.get("PARTNERS_PORTAL_URL")
)
}
// check if status of active requires notification
else if (listingData.status === ListingStatus.active) {
const previousStatus = await this.listingRepository
.createQueryBuilder("listings")
.select("listings.status")
.where("id = :id", { id: listingData.id })
.getOne()
result = await this.update(listingData, user)
// if not new published listing, skip notification and return update response
else if (params.status === ListingStatus.active) {
// if newly published listing, notify non-approving users with access
if (
previousStatus.status !== ListingStatus.pendingReview &&
previousStatus.status !== ListingStatus.changesRequested
params.previousStatus === ListingStatus.pendingReview ||
params.previousStatus === ListingStatus.changesRequested ||
params.previousStatus === ListingStatus.pending
) {
return result
const userInfo = await this.getUserEmailInfo(
nonApprovingRoles,
params.listingInfo.id,
params.jurisId,
true
)
await this.emailService.listingApproved(
params.user,
{ id: params.listingInfo.id, name: params.listingInfo.name },
userInfo.emails,
userInfo.publicUrl
)
}
// otherwise get user info and send listing approved email
const nonApprovingUserInfo = await this.getNonApprovingUserInfo(
listingData.id,
listingData.jurisdiction.id,
true
)
await this.emailService.listingApproved(
user,
{ id: listingData.id, name: listingData.name },
nonApprovingUserInfo.emails,
nonApprovingUserInfo.publicUrl
)
} else {
result = await this.update(listingData, user)
}
return result
}

async rawListWithFlagged() {
Expand Down
Loading

0 comments on commit b45211e

Please sign in to comment.