Skip to content

Commit

Permalink
feat(backend): Add user delete endpoint and expose leasingAgentInList… (
Browse files Browse the repository at this point in the history
bloom-housing#1996)

* feat(backend): Add user delete endpoint and expose leasingAgentInListings in UserUpdateDto

* feat(backend): Add DELETE and GET /user/:id endpoints

* fix(backend): set bigger timeout for email.service.spec.ts

* test(backend): fix SETEX impossible to process

* Fix code style issues with Prettier

Co-authored-by: Lint Action <[email protected]>
  • Loading branch information
pbn4 and lint-action authored Oct 20, 2021
1 parent f313775 commit a13f735
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ All notable changes to this project will be documented in this file. The format
- Added the optional jurisdiction setting notificationsSignUpURL, which now appears on the home page if set ([#1802](https://github.com/bloom-housing/bloom/pull/1802)) (Emily Jablonski)
- Adds Listings managment validations required for publishing a Listing [#1850](https://github.com/bloom-housing/bloom/pull/1850) (Michał Plebański & Emily Jablonski)
- Add UnitCreateDto model changes to prevent form submission from creating UnitType, UnitRentType and AccessibilityType from creating a new DB row on each submission. ([#1956](https://github.com/bloom-housing/bloom/pull/1956))
- Add `DELETE /user/:id` and `GET /user/:id` endpoints and add leasingAgentInListings to UserUpdateDto

- Changed:

Expand Down
15 changes: 15 additions & 0 deletions backend/core/src/auth/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
Expand Down Expand Up @@ -132,4 +134,17 @@ export class UserController {
await this.userService.invite(dto, new AuthContext(req.user as User))
)
}

@Get(`:userId`)
@ApiOperation({ summary: "Get user by id", operationId: "retrieve" })
async retrieve(@Param("userId") userId: string): Promise<UserDto> {
return mapTo(UserDto, await this.userService.findOneOrFail({ id: userId }))
}

@Delete(`:userId`)
@UseGuards(OptionalAuthGuard, AuthzGuard)
@ApiOperation({ summary: "Delete user by id", operationId: "delete" })
async delete(@Param("userId") userId: string): Promise<void> {
return await this.userService.delete(userId)
}
}
2 changes: 1 addition & 1 deletion backend/core/src/auth/dto/user-invite.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class UserInviteDto extends OmitType(UserDto, [
jurisdictions: IdDto[]

@Expose()
@IsOptional()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => IdDto)
Expand Down
7 changes: 7 additions & 0 deletions backend/core/src/auth/dto/user-update.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,11 @@ export class UserUpdateDto extends OmitType(UserDto, [
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => IdDto)
jurisdictions: IdDto[]

@Expose()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
@IsDefined({ groups: [ValidationsGroupsEnum.default] })
@ValidateNested({ groups: [ValidationsGroupsEnum.default], each: true })
@Type(() => IdDto)
leasingAgentInListings?: IdDto[] | null
}
2 changes: 2 additions & 0 deletions backend/core/src/auth/entities/user-roles.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { User } from "./user.entity"
export class UserRoles {
@OneToOne(() => User, (user) => user.roles, {
primary: true,
onDelete: "CASCADE",
onUpdate: "CASCADE",
})
@JoinColumn()
user: User
Expand Down
2 changes: 2 additions & 0 deletions backend/core/src/auth/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export class User {
eager: true,
cascade: true,
nullable: true,
onDelete: "CASCADE",
onUpdate: "CASCADE",
})
@Expose()
roles?: UserRoles
Expand Down
16 changes: 16 additions & 0 deletions backend/core/src/auth/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ export class UserService {
return this.userRepository.findOne({ where: options, relations: ["leasingAgentInListings"] })
}

public async findOneOrFail(options: FindConditions<User>) {
const user = await this.find(options)
if (!user) {
throw new NotFoundException()
}
return user
}

public async list(
params: UserListQueryParams,
authContext: AuthContext
Expand Down Expand Up @@ -330,4 +338,12 @@ export class UserService {
)
return user
}

async delete(userId: string) {
const user = await this.userRepository.findOne({ id: userId })
if (!user) {
throw new NotFoundException()
}
await this.userRepository.remove(user)
}
}
12 changes: 10 additions & 2 deletions backend/core/src/listings/listings.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CacheModule, CACHE_MANAGER, Inject, Module, OnModuleDestroy } from "@nestjs/common"
import {
CacheModule,
CACHE_MANAGER,
Inject,
Module,
OnModuleDestroy,
OnModuleInit,
} from "@nestjs/common"
import { TypeOrmModule } from "@nestjs/typeorm"
import * as redisStore from "cache-manager-redis-store"
import { Store } from "cache-manager"
Expand Down Expand Up @@ -50,7 +57,7 @@ if (process.env.REDIS_USE_TLS !== "0") {
controllers: [ListingsController],
})
// We have to manually disconnect from redis on app close
export class ListingsModule implements OnModuleDestroy {
export class ListingsModule implements OnModuleDestroy, OnModuleInit {
redisClient: Redis.RedisClient
constructor(@Inject(CACHE_MANAGER) private cacheManager: RedisCache) {
this.redisClient = this.cacheManager.store.getClient()
Expand All @@ -61,6 +68,7 @@ export class ListingsModule implements OnModuleDestroy {
}
onModuleDestroy() {
console.log("Disconnect from Redis")
void this.cacheManager.store.reset()
this.redisClient.quit()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm"

export class addCascadeToUserRolesUserRelation1634210584036 implements MigrationInterface {
name = "addCascadeToUserRolesUserRelation1634210584036"

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "user_roles" DROP CONSTRAINT IF EXISTS "FK_87b8888186ca9769c960e926870"`)
await queryRunner.query(
`ALTER TABLE "user_roles" ADD CONSTRAINT "FK_87b8888186ca9769c960e926870" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE`
)
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "user_roles" DROP CONSTRAINT IF EXISTS "FK_87b8888186ca9769c960e926870"`)
await queryRunner.query(
`ALTER TABLE "user_roles" ADD CONSTRAINT "FK_87b8888186ca9769c960e926870" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
}
}
1 change: 1 addition & 0 deletions backend/core/src/shared/email/email.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { JurisdictionsService } from "../../jurisdictions/services/jurisdictions
import { Jurisdiction } from "../../jurisdictions/entities/jurisdiction.entity"

declare const expect: jest.Expect
jest.setTimeout(30000)
const user = new User()
user.firstName = "Test"
user.lastName = "User"
Expand Down
35 changes: 35 additions & 0 deletions backend/core/test/user/user.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,4 +570,39 @@ describe("Applications", () => {
true
)
})

it("should get and delete a user by ID", async () => {
const user = await userService._createUser(
{
dob: new Date(),
email: "[email protected]",
firstName: "test",
jurisdictions: [],
language: Language.en,
lastName: "",
middleName: "",
roles: { isPartner: true, isAdmin: false },
updatedAt: undefined,
passwordHash: "abcd",
},
null
)

const res = await supertest(app.getHttpServer())
.get(`/user/${user.id}`)
.set(...setAuthorization(adminAccessToken))
.expect(200)
expect(res.body.id).toBe(user.id)
expect(res.body.email).toBe(user.email)

await supertest(app.getHttpServer())
.delete(`/user/${user.id}`)
.set(...setAuthorization(adminAccessToken))
.expect(200)

await supertest(app.getHttpServer())
.get(`/user/${user.id}`)
.set(...setAuthorization(adminAccessToken))
.expect(404)
})
})
47 changes: 47 additions & 0 deletions backend/core/types/src/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,50 @@ export class UserService {

let data = params.body

configs.data = data
axios(configs, resolve, reject)
})
}
/**
* Get user by id
*/
retrieve(
params: {
/** */
userId: string
} = {} as any,
options: IRequestOptions = {}
): Promise<User> {
return new Promise((resolve, reject) => {
let url = basePath + "/user/{userId}"
url = url.replace("{userId}", params["userId"] + "")

const configs: IRequestConfig = getConfigs("get", "application/json", url, options)

let data = null

configs.data = data
axios(configs, resolve, reject)
})
}
/**
* Delete user by id
*/
delete(
params: {
/** */
userId: string
} = {} as any,
options: IRequestOptions = {}
): Promise<any> {
return new Promise((resolve, reject) => {
let url = basePath + "/user/{userId}"
url = url.replace("{userId}", params["userId"] + "")

const configs: IRequestConfig = getConfigs("delete", "application/json", url, options)

let data = null

configs.data = data
axios(configs, resolve, reject)
})
Expand Down Expand Up @@ -3826,6 +3870,9 @@ export interface UserUpdate {
/** */
jurisdictions: Id[]

/** */
leasingAgentInListings?: Id[]

/** */
confirmedAt?: Date

Expand Down

0 comments on commit a13f735

Please sign in to comment.