diff --git a/backend/src/external_api/css/css.service.ts b/backend/src/external_api/css/css.service.ts index 540667959..01e28b6d7 100644 --- a/backend/src/external_api/css/css.service.ts +++ b/backend/src/external_api/css/css.service.ts @@ -3,6 +3,7 @@ import { ExternalApiService } from "../external-api-service"; import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { get } from "../../helpers/axios-api"; import { ConfigurationService } from "../../v1/configuration/configuration.service"; +import { CssUser } from "src/types/css/cssUser"; @Injectable() export class CssService implements ExternalApiService { @@ -64,6 +65,24 @@ export class CssService implements ExternalApiService { } }; + getUserIdirByEmail = async (email: string): Promise => { + try { + const apiToken = await this.authenticate(); + const url = `${this.baseUri}/api/v1/${this.env}/idir/users?email=${email}`; + const config: AxiosRequestConfig = { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiToken}`, + }, + }; + const response = await get(apiToken, url, config); + return response?.data.data; + } catch (error) { + this.logger.error(`exception: unable to get user by email: ${email} - error: ${error}`); + throw new Error(`exception: unable to get user by email: ${email} - error: ${error}`); + } + }; + getUserRoles = async (userIdir): Promise<{ name: string; composite: string }[]> => { try { const apiToken = await this.authenticate(); diff --git a/backend/src/types/css/cssUser.ts b/backend/src/types/css/cssUser.ts new file mode 100644 index 000000000..83bc71c46 --- /dev/null +++ b/backend/src/types/css/cssUser.ts @@ -0,0 +1,11 @@ +export interface CssUser { + username: string; + firstName: string; + lastName: string; + email: string; + attributes: { + idir_user_guid: string[]; + idir_username: string[]; + display_name: string[]; + }; +} diff --git a/backend/src/types/models/general/team-update.ts b/backend/src/types/models/general/team-update.ts new file mode 100644 index 000000000..c39d1b453 --- /dev/null +++ b/backend/src/types/models/general/team-update.ts @@ -0,0 +1,7 @@ +export interface TeamUpdate { + userIdir: string; + adminIdirUsername: string; + agencyCode: string; + teamCode: string | null; + roles: Array<{ name: string }>; +} diff --git a/backend/src/types/models/people/officer.ts b/backend/src/types/models/people/officer.ts new file mode 100644 index 000000000..e98f0cc9e --- /dev/null +++ b/backend/src/types/models/people/officer.ts @@ -0,0 +1,26 @@ +export interface NewOfficer { + user_id: string; + create_user_id: string; + create_utc_timestamp: Date; + update_user_id: string; + update_utc_timestamp: Date; + auth_user_guid: string; + office_guid: string | null; + team_code: string | null; + person_guid: { + first_name: string; + middle_name_1: null; + middle_name_2: null; + last_name: string; + create_user_id: string; + create_utc_timestamp: Date; + update_user_id: string; + updateTimestamp: Date; + }; + roles: { + user_roles: Array<{ name: string | undefined }>; + user_idir: string; //Example : fohe4m5pn8clhkxmlho33sn1r7vr7m67@idir + }; + coms_enrolled_ind: boolean; + deactivate_ind: boolean; +} diff --git a/backend/src/v1/case_file/case_file.service.spec.ts b/backend/src/v1/case_file/case_file.service.spec.ts index ae75c772a..a0355b4f5 100644 --- a/backend/src/v1/case_file/case_file.service.spec.ts +++ b/backend/src/v1/case_file/case_file.service.spec.ts @@ -72,6 +72,10 @@ import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; import { Person } from "../person/entities/person.entity"; import { LinkedComplaintXrefService } from "../linked_complaint_xref/linked_complaint_xref.service"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { Team } from "../team/entities/team.entity"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; describe("Testing: Case File Service", () => { let service: CaseFileService; @@ -208,6 +212,14 @@ describe("Testing: Case File Service", () => { provide: getRepositoryToken(Person), useValue: {}, }, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, ComplaintUpdatesService, CaseFileService, ComplaintService, @@ -221,6 +233,8 @@ describe("Testing: Case File Service", () => { PersonComplaintXrefService, AttractantHwcrXrefService, CompMthdRecvCdAgcyCdXrefService, + TeamService, + OfficerTeamXrefService, { provide: REQUEST, useValue: { diff --git a/backend/src/v1/complaint/complaint.service.spec.ts b/backend/src/v1/complaint/complaint.service.spec.ts index cf42abcb6..b81827bc8 100644 --- a/backend/src/v1/complaint/complaint.service.spec.ts +++ b/backend/src/v1/complaint/complaint.service.spec.ts @@ -82,6 +82,10 @@ import { Person } from "../person/entities/person.entity"; import { Configuration } from "../configuration/entities/configuration.entity"; import { LinkedComplaintXrefService } from "../linked_complaint_xref/linked_complaint_xref.service"; import { LinkedComplaintXref } from "../linked_complaint_xref/entities/linked_complaint_xref.entity"; +import { TeamService } from "../team/team.service"; +import { Team } from "../team/entities/team.entity"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; describe("Testing: Complaint Service", () => { let service: ComplaintService; @@ -117,6 +121,14 @@ describe("Testing: Complaint Service", () => { provide: getRepositoryToken(LinkedComplaintXref), useValue: {}, }, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, PersonComplaintXrefService, @@ -129,6 +141,8 @@ describe("Testing: Complaint Service", () => { AttractantHwcrXrefService, CodeTableService, CompMthdRecvCdAgcyCdXrefService, + TeamService, + OfficerTeamXrefService, { provide: getRepositoryToken(Complaint), useFactory: MockComplaintsRepositoryV2, @@ -410,6 +424,14 @@ describe("Testing: Complaint Service", () => { provide: getRepositoryToken(LinkedComplaintXref), useValue: {}, }, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, PersonComplaintXrefService, @@ -422,6 +444,8 @@ describe("Testing: Complaint Service", () => { AttractantHwcrXrefService, CodeTableService, CompMthdRecvCdAgcyCdXrefService, + TeamService, + OfficerTeamXrefService, { provide: getRepositoryToken(Complaint), useFactory: MockUpdateComplaintsRepository, diff --git a/backend/src/v1/document/document.controller.spec.ts b/backend/src/v1/document/document.controller.spec.ts index d1c7ad775..07acea7ce 100644 --- a/backend/src/v1/document/document.controller.spec.ts +++ b/backend/src/v1/document/document.controller.spec.ts @@ -74,6 +74,10 @@ import { CssService } from "../../external_api/css/css.service"; import { Person } from "../person/entities/person.entity"; import { LinkedComplaintXref } from "../linked_complaint_xref/entities/linked_complaint_xref.entity"; import { LinkedComplaintXrefService } from "../linked_complaint_xref/linked_complaint_xref.service"; +import { Team } from "../team/entities/team.entity"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; describe("DocumentController", () => { let controller: DocumentController; @@ -206,6 +210,14 @@ describe("DocumentController", () => { provide: getRepositoryToken(LinkedComplaintXref), useValue: {}, }, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, CodeTableService, @@ -217,6 +229,8 @@ describe("DocumentController", () => { PersonService, AttractantHwcrXrefService, CompMthdRecvCdAgcyCdXrefService, + TeamService, + OfficerTeamXrefService, { provide: REQUEST, useValue: { diff --git a/backend/src/v1/document/document.service.spec.ts b/backend/src/v1/document/document.service.spec.ts index f6b60a4ed..75fc30e07 100644 --- a/backend/src/v1/document/document.service.spec.ts +++ b/backend/src/v1/document/document.service.spec.ts @@ -73,6 +73,10 @@ import { CssService } from "../../external_api/css/css.service"; import { Person } from "../person/entities/person.entity"; import { LinkedComplaintXrefService } from "../linked_complaint_xref/linked_complaint_xref.service"; import { LinkedComplaintXref } from "../linked_complaint_xref/entities/linked_complaint_xref.entity"; +import { Team } from "../team/entities/team.entity"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; describe("DocumentService", () => { let service: DocumentService; @@ -205,6 +209,14 @@ describe("DocumentService", () => { provide: getRepositoryToken(LinkedComplaintXref), useValue: {}, }, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, CodeTableService, @@ -216,6 +228,8 @@ describe("DocumentService", () => { PersonService, AttractantHwcrXrefService, CompMthdRecvCdAgcyCdXrefService, + TeamService, + OfficerTeamXrefService, { provide: REQUEST, useValue: { diff --git a/backend/src/v1/officer/dto/create-officer.dto.ts b/backend/src/v1/officer/dto/create-officer.dto.ts index 33ecc2fe8..e14ea120d 100644 --- a/backend/src/v1/officer/dto/create-officer.dto.ts +++ b/backend/src/v1/officer/dto/create-officer.dto.ts @@ -5,8 +5,11 @@ export class CreateOfficerDto extends PickType(OfficerDto, [ "user_id", "person_guid", "office_guid", + "auth_user_guid", "create_user_id", "create_utc_timestamp", "update_user_id", "update_utc_timestamp", + "coms_enrolled_ind", + "deactivate_ind", ] as const) {} diff --git a/backend/src/v1/officer/dto/officer.dto.ts b/backend/src/v1/officer/dto/officer.dto.ts index 8afe04cf4..48434bdd7 100644 --- a/backend/src/v1/officer/dto/officer.dto.ts +++ b/backend/src/v1/officer/dto/officer.dto.ts @@ -57,4 +57,16 @@ export class OfficerDto { description: "The keycloak guid for the officer", }) auth_user_guid: UUID; + + @ApiProperty({ + example: "true", + description: "An indicator to determine if the officer has access to COMS", + }) + coms_enrolled_ind: boolean; + + @ApiProperty({ + example: "false", + description: "An indicator to determine if the officer has been deactivated", + }) + deactivate_ind: boolean; } diff --git a/backend/src/v1/officer/dto/update-officer.dto.ts b/backend/src/v1/officer/dto/update-officer.dto.ts index 3c23898bf..77d819844 100644 --- a/backend/src/v1/officer/dto/update-officer.dto.ts +++ b/backend/src/v1/officer/dto/update-officer.dto.ts @@ -1,4 +1,6 @@ import { PartialType } from "@nestjs/swagger"; import { CreateOfficerDto } from "./create-officer.dto"; -export class UpdateOfficerDto extends PartialType(CreateOfficerDto) {} +export class UpdateOfficerDto extends PartialType(CreateOfficerDto) { + user_roles?: string[]; +} diff --git a/backend/src/v1/officer/entities/officer.entity.ts b/backend/src/v1/officer/entities/officer.entity.ts index 64b050523..f79fb5f63 100644 --- a/backend/src/v1/officer/entities/officer.entity.ts +++ b/backend/src/v1/officer/entities/officer.entity.ts @@ -79,6 +79,13 @@ export class Officer { @Column() coms_enrolled_ind: boolean; + @ApiProperty({ + example: false, + description: "Indicates whether an officer has been deactivated", + }) + @Column() + deactivate_ind: boolean; + user_roles: string[]; @AfterLoad() updateUserRoles() { diff --git a/backend/src/v1/officer/officer.controller.spec.ts b/backend/src/v1/officer/officer.controller.spec.ts index de6231cf9..7e50f58af 100644 --- a/backend/src/v1/officer/officer.controller.spec.ts +++ b/backend/src/v1/officer/officer.controller.spec.ts @@ -12,6 +12,10 @@ import { dataSourceMockFactory } from "../../../test/mocks/datasource"; import { CssService } from "../../external_api/css/css.service"; import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; +import { Team } from "../team/entities/team.entity"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; describe("OfficerController", () => { let controller: OfficerController; @@ -35,6 +39,16 @@ describe("OfficerController", () => { provide: getRepositoryToken(Office), useValue: {}, }, + TeamService, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + OfficerTeamXrefService, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, { provide: DataSource, useFactory: dataSourceMockFactory, diff --git a/backend/src/v1/officer/officer.controller.ts b/backend/src/v1/officer/officer.controller.ts index 12fc38cc7..86088e875 100644 --- a/backend/src/v1/officer/officer.controller.ts +++ b/backend/src/v1/officer/officer.controller.ts @@ -1,6 +1,5 @@ import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Put } from "@nestjs/common"; import { OfficerService } from "./officer.service"; -import { CreateOfficerDto } from "./dto/create-officer.dto"; import { UpdateOfficerDto } from "./dto/update-officer.dto"; import { Roles } from "../../auth/decorators/roles.decorator"; import { Role } from "../../enum/role.enum"; @@ -9,6 +8,7 @@ import { ApiTags } from "@nestjs/swagger"; import { UUID } from "crypto"; import { User } from "../../auth/decorators/user.decorator"; import { Token } from "../../auth/decorators/token.decorator"; +import { NewOfficer } from "../../types/models/people/officer"; @ApiTags("officer") @UseGuards(JwtRoleGuard) @@ -21,7 +21,7 @@ export class OfficerController { @Post() @Roles(Role.COS_OFFICER) - create(@Body() createOfficerDto: CreateOfficerDto) { + create(@Body() createOfficerDto: NewOfficer) { return this.officerService.create(createOfficerDto); } @@ -61,6 +61,12 @@ export class OfficerController { return this.officerService.findByPersonGuid(person_guid); } + @Get("/find-by-email/:email") + @Roles(Role.TEMPORARY_TEST_ADMIN) + findUserByEmail(@Param("email") email: string) { + return this.officerService.findByCssEmail(email); + } + @Patch(":id") @Roles(Role.COS_OFFICER, Role.CEEB, Role.TEMPORARY_TEST_ADMIN) update(@Param("id") id: UUID, @Body() updateOfficerDto: UpdateOfficerDto) { diff --git a/backend/src/v1/officer/officer.module.ts b/backend/src/v1/officer/officer.module.ts index 8b4561be9..627fd7141 100644 --- a/backend/src/v1/officer/officer.module.ts +++ b/backend/src/v1/officer/officer.module.ts @@ -7,17 +7,23 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Officer } from "./entities/officer.entity"; import { Person } from "../person/entities/person.entity"; import { Office } from "../office/entities/office.entity"; -import { CssModule } from "src/external_api/css/css.module"; +import { CssModule } from "../../external_api/css/css.module"; +import { TeamService } from "../team/team.service"; +import { Team } from "../team/entities/team.entity"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; @Module({ imports: [ TypeOrmModule.forFeature([Officer]), TypeOrmModule.forFeature([Person]), TypeOrmModule.forFeature([Office]), + TypeOrmModule.forFeature([Team]), + TypeOrmModule.forFeature([OfficerTeamXref]), CssModule, ], controllers: [OfficerController], - providers: [OfficerService, PersonService, OfficeService], + providers: [OfficerService, PersonService, OfficeService, TeamService, OfficerTeamXrefService], exports: [OfficerService], }) export class OfficerModule {} diff --git a/backend/src/v1/officer/officer.service.spec.ts b/backend/src/v1/officer/officer.service.spec.ts index 7b6e1d82b..31ef3346b 100644 --- a/backend/src/v1/officer/officer.service.spec.ts +++ b/backend/src/v1/officer/officer.service.spec.ts @@ -11,6 +11,10 @@ import { dataSourceMockFactory } from "../../../test/mocks/datasource"; import { CssService } from "../../external_api/css/css.service"; import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; +import { Team } from "../team/entities/team.entity"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; describe("OfficerService", () => { let service: OfficerService; @@ -33,6 +37,16 @@ describe("OfficerService", () => { provide: getRepositoryToken(Office), useValue: {}, }, + TeamService, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + OfficerTeamXrefService, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, { provide: DataSource, useFactory: dataSourceMockFactory, diff --git a/backend/src/v1/officer/officer.service.ts b/backend/src/v1/officer/officer.service.ts index b9ad26c4b..26c755efb 100644 --- a/backend/src/v1/officer/officer.service.ts +++ b/backend/src/v1/officer/officer.service.ts @@ -3,8 +3,6 @@ import { CreateOfficerDto } from "./dto/create-officer.dto"; import { CreatePersonDto } from "../person/dto/create-person.dto"; import { UpdateOfficerDto } from "./dto/update-officer.dto"; import { Officer } from "./entities/officer.entity"; -import { Office } from "../office/entities/office.entity"; -import { AgencyCode } from "../agency_code/entities/agency_code.entity"; import { InjectRepository } from "@nestjs/typeorm"; import { DataSource, Repository } from "typeorm"; import { PersonService } from "../person/person.service"; @@ -13,18 +11,28 @@ import { UUID } from "crypto"; import { CssService } from "../../external_api/css/css.service"; import { Role } from "../../enum/role.enum"; import { put } from "../../helpers/axios-api"; +import { CssUser } from "../../types/css/cssUser"; +import { CreateOfficerTeamXrefDto } from "../officer_team_xref/dto/create-officer_team_xref.dto"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { NewOfficer } from "../../types/models/people/officer"; @Injectable() export class OfficerService { private readonly logger = new Logger(OfficerService.name); - constructor(private dataSource: DataSource) {} + constructor(private readonly dataSource: DataSource) {} @InjectRepository(Officer) - private officerRepository: Repository; + private readonly officerRepository: Repository; + @Inject(PersonService) protected readonly personService: PersonService; @Inject(OfficeService) protected readonly officeService: OfficeService; + @Inject(TeamService) + protected readonly teamService: TeamService; + @Inject(OfficerTeamXrefService) + protected readonly officerTeamXrefService: OfficerTeamXrefService; @Inject(CssService) private readonly cssService: CssService; @@ -55,6 +63,8 @@ export class OfficerService { update_user_id: officer.update_user_id, update_utc_timestamp: officer.update_utc_timestamp, auth_user_guid: officer.auth_user_guid, + coms_enrolled_ind: officer.coms_enrolled_ind, + deactivate_ind: officer.deactivate_ind, user_roles: roleMapping[useGuid] ?? [], } as Officer; }); @@ -93,6 +103,20 @@ export class OfficerService { }); } + async findByCssEmail(email: string): Promise { + const cssUser = await this.cssService.getUserIdirByEmail(email); + if (cssUser.length === 0) return null; + if (cssUser.length > 0) { + //assume email is unique and return only 1 result + const officer = await this.findByAuthUserGuid(cssUser[0].attributes.idir_user_guid[0]); + //if user already exists in officer table, then return officer data + if (officer) { + return officer; + } + } + return cssUser[0]; + } + async findByUserId(userid: string): Promise { userid = userid.toUpperCase(); return this.officerRepository.findOne({ @@ -119,60 +143,75 @@ export class OfficerService { }); } - async create(officer: any): Promise { + async create(officer: NewOfficer): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); - let newOfficerString; - let officeObject; + let newOfficerObject; let personObject; try { - //Look for the Office - officeObject = await this.officeService.findByGeoOrgCode(officer.geo_organization_unit_code); - if (officeObject.length === 0) { - // insertOffice - - let agencyObject = new AgencyCode("COS"); - let officeObject = new Office(); - - officeObject.agency_code = agencyObject; - officeObject.cos_geo_org_unit = officer.geo_organization_unit_code; - officeObject.create_user_id = officer.create_user_id; - officeObject.create_utc_timestamp = officer.create_utc_timestamp; - officeObject.update_user_id = officer.update_user_id; - officeObject.update_utc_timestamp = officer.update_utc_timestamp; - - officeObject = await this.officeService.create(officeObject); - officer.office_guid = officeObject.office_guid; - } else { - // use the existing one - officer.office_guid = officeObject[0].office_guid; - } - //Will always insert the person - personObject = await this.personService.createInTransaction(officer, queryRunner); + personObject = await this.personService.createInTransaction( + (officer.person_guid), + queryRunner, + ); officer.person_guid = personObject.person_guid; - newOfficerString = await this.officerRepository.create(officer); - await queryRunner.manager.save(newOfficerString); + newOfficerObject = this.officerRepository.create((officer)); + await queryRunner.manager.save(newOfficerObject); + + //Create team + if (officer.team_code) { + const teamGuid = await this.teamService.findByTeamCodeAndAgencyCode(officer.team_code, "EPO"); + const teamEntity = { + officer_guid: newOfficerObject.officer_guid, + team_guid: teamGuid, + active_ind: true, + create_user_id: officer.create_user_id, + update_user_id: officer.update_user_id, + }; + await this.officerTeamXrefService.createInTransaction(teamEntity, queryRunner); + } await queryRunner.commitTransaction(); + + //Create roles + await this.cssService.updateUserRole(officer.roles.user_idir, officer.roles.user_roles); } catch (err) { this.logger.error(err); + //rollback all transactions await queryRunner.rollbackTransaction(); - newOfficerString = "Error Occured"; + newOfficerObject = null; + //remove all css roles + for await (const roleItem of officer.roles.user_roles) { + await this.cssService.deleteUserRole(officer.roles.user_idir, roleItem.name); + } } finally { await queryRunner.release(); } - return newOfficerString; + return newOfficerObject; } async update(officer_guid: UUID, updateOfficerDto: UpdateOfficerDto): Promise { + const userRoles = updateOfficerDto.user_roles; //exclude roles field populated from keycloak from update delete (updateOfficerDto as any).user_roles; - await this.officerRepository.update({ officer_guid }, updateOfficerDto); - return this.findOne(officer_guid); + + try { + await this.officerRepository.update({ officer_guid }, updateOfficerDto); + + //remove all roles if deactivate_ind is true + if (updateOfficerDto.deactivate_ind === true) { + const officerIdirUsername = `${updateOfficerDto.auth_user_guid.split("-").join("")}@idir`; + for await (const roleItem of userRoles) { + await this.cssService.deleteUserRole(officerIdirUsername, roleItem); + } + } + return this.findOne(officer_guid); + } catch (e) { + this.logger.error(e); + } } /** diff --git a/backend/src/v1/officer/officer.service.v2.spec.ts b/backend/src/v1/officer/officer.service.v2.spec.ts index e796ac6da..38434614b 100644 --- a/backend/src/v1/officer/officer.service.v2.spec.ts +++ b/backend/src/v1/officer/officer.service.v2.spec.ts @@ -18,6 +18,10 @@ import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; import { MockRoleRepository } from "../../../test/mocks/mock-role-repository"; import { REQUEST } from "@nestjs/core"; +import { Team } from "../team/entities/team.entity"; +import { TeamService } from "../team/team.service"; +import { OfficerTeamXrefService } from "../officer_team_xref/officer_team_xref.service"; +import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; describe("Testing: OfficerService", () => { let service: OfficerService; @@ -54,6 +58,16 @@ describe("Testing: OfficerService", () => { provide: getRepositoryToken(Configuration), useValue: {}, }, + TeamService, + { + provide: getRepositoryToken(Team), + useValue: {}, + }, + OfficerTeamXrefService, + { + provide: getRepositoryToken(OfficerTeamXref), + useValue: {}, + }, ], }) .compile() diff --git a/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts b/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts index 70b9261a6..8404e8e22 100644 --- a/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts +++ b/backend/src/v1/officer_team_xref/dto/create-officer_team_xref.dto.ts @@ -2,7 +2,6 @@ import { PickType } from "@nestjs/swagger"; import { OfficerTeamXrefDto } from "./officer_team_xref.dto"; export class CreateOfficerTeamXrefDto extends PickType(OfficerTeamXrefDto, [ - "officer_team_xref_guid", "officer_guid", "team_guid", "create_user_id", diff --git a/backend/src/v1/officer_team_xref/officer_team_xref.service.ts b/backend/src/v1/officer_team_xref/officer_team_xref.service.ts index 910656673..c83557013 100644 --- a/backend/src/v1/officer_team_xref/officer_team_xref.service.ts +++ b/backend/src/v1/officer_team_xref/officer_team_xref.service.ts @@ -1,23 +1,45 @@ -import { Inject, Injectable, Logger } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { CreateOfficerTeamXrefDto } from "./dto/create-officer_team_xref.dto"; import { UpdateOfficerTeamXrefDto } from "./dto/update-officer_team_xref.dto"; import { OfficerTeamXref } from "./entities/officer_team_xref.entity"; import { DataSource, QueryRunner, Repository } from "typeorm"; import { InjectRepository } from "@nestjs/typeorm"; -import { REQUEST } from "@nestjs/core"; @Injectable() export class OfficerTeamXrefService { private readonly logger = new Logger(OfficerTeamXrefService.name); @InjectRepository(OfficerTeamXref) - private officerTeamXrefRepository: Repository; + private readonly officerTeamXrefRepository: Repository; - constructor(@Inject(REQUEST) private request: Request, private dataSource: DataSource) {} + constructor(private readonly dataSource: DataSource) {} - async create(queryRunner: QueryRunner, createOfficerTeamXrefDto: CreateOfficerTeamXrefDto) { - const createdValue = await this.officerTeamXrefRepository.create(createOfficerTeamXrefDto); - queryRunner.manager.save(createdValue); - return createdValue; + async create(newOfficerTeamXref: CreateOfficerTeamXrefDto): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + let result; + try { + result = this.officerTeamXrefRepository.create(newOfficerTeamXref); + await queryRunner.manager.save(result); + await queryRunner.commitTransaction(); + } catch (err) { + this.logger.error(err); + await queryRunner.rollbackTransaction(); + result = null; + } finally { + await queryRunner.release(); + } + return result; + } + + async createInTransaction( + officerTeamXref: CreateOfficerTeamXrefDto, + queryRunner: QueryRunner, + ): Promise { + const newTeam = this.officerTeamXrefRepository.create(officerTeamXref); + await queryRunner.manager.save(newTeam); + return newTeam; } async findAll(): Promise { diff --git a/backend/src/v1/person/person.service.ts b/backend/src/v1/person/person.service.ts index 0f18dea43..87e8ee35a 100644 --- a/backend/src/v1/person/person.service.ts +++ b/backend/src/v1/person/person.service.ts @@ -10,9 +10,9 @@ import { UUID } from "crypto"; export class PersonService { private readonly logger = new Logger(PersonService.name); - constructor(private dataSource: DataSource) {} + constructor(private readonly dataSource: DataSource) {} @InjectRepository(Person) - private personRepository: Repository; + private readonly personRepository: Repository; async create(person: any): Promise { const queryRunner = this.dataSource.createQueryRunner(); @@ -21,7 +21,7 @@ export class PersonService { await queryRunner.startTransaction(); let newPersonString; try { - newPersonString = await this.personRepository.create(person); + newPersonString = this.personRepository.create(person); await queryRunner.manager.save(newPersonString); await queryRunner.commitTransaction(); } catch (err) { @@ -35,7 +35,7 @@ export class PersonService { } async createInTransaction(person: CreatePersonDto, queryRunner: QueryRunner): Promise { - const newPerson = await this.personRepository.create(person); + const newPerson = this.personRepository.create(person); await queryRunner.manager.save(newPerson); return newPerson; } diff --git a/backend/src/v1/team/team.controller.spec.ts b/backend/src/v1/team/team.controller.spec.ts index 35edefb9c..822de1f37 100644 --- a/backend/src/v1/team/team.controller.spec.ts +++ b/backend/src/v1/team/team.controller.spec.ts @@ -9,6 +9,7 @@ import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref import { CssService } from "../../external_api/css/css.service"; import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; +import { Officer } from "../officer/entities/officer.entity"; describe("TeamController", () => { let controller: TeamController; @@ -26,6 +27,10 @@ describe("TeamController", () => { provide: getRepositoryToken(OfficerTeamXref), useValue: {}, }, + { + provide: getRepositoryToken(Officer), + useValue: {}, + }, { provide: DataSource, useFactory: dataSourceMockFactory, diff --git a/backend/src/v1/team/team.controller.ts b/backend/src/v1/team/team.controller.ts index 29f0d092a..671687e49 100644 --- a/backend/src/v1/team/team.controller.ts +++ b/backend/src/v1/team/team.controller.ts @@ -5,6 +5,7 @@ import { UUID } from "crypto"; import { Roles } from "../../auth/decorators/roles.decorator"; import { Role } from "../../enum/role.enum"; import { JwtRoleGuard } from "../../auth/jwtrole.guard"; +import { TeamUpdate } from "src/types/models/general/team-update"; @UseGuards(JwtRoleGuard) @ApiTags("team") @@ -23,37 +24,13 @@ export class TeamController { @Get("current") @Roles(Role.TEMPORARY_TEST_ADMIN) - async findCurrentTeamAndRole(@Query("userIdir") userIdir: string, @Query("officerGuid") officerGuid: UUID) { - let result = { - agency: null, - team: null, - roles: null, - }; - const currentRoles = await this.teamService.findUserCurrentRoles(userIdir); - result.roles = currentRoles; - const hasCEEBRole = currentRoles.findIndex( - (role) => - role.name === Role.CEEB || - role.name === Role.CEEB_COMPLIANCE_COORDINATOR || - role.name === Role.CEEB_SECTION_HEAD, - ); - if (hasCEEBRole > -1) { - result.agency = "EPO"; - result.team = await this.teamService.findUserCurrentTeam(officerGuid); - } - - const hasCOSRole = currentRoles.findIndex( - (role: any) => role.name === Role.COS_ADMINISTRATOR || role.name === Role.COS_OFFICER, - ); - if (hasCOSRole > -1) { - result.agency = "COS"; - } - return result; + async findCurrentTeam(@Query("officerGuid") officerGuid: UUID) { + return await this.teamService.findUserCurrentTeam(officerGuid); } @Patch("update/:officer_guid") @Roles(Role.TEMPORARY_TEST_ADMIN) - update(@Param("officer_guid") officerGuid: UUID, @Body() updateTeamData: any) { + update(@Param("officer_guid") officerGuid: UUID, @Body() updateTeamData: TeamUpdate) { return this.teamService.update(officerGuid, updateTeamData); } } diff --git a/backend/src/v1/team/team.module.ts b/backend/src/v1/team/team.module.ts index e53b179f3..85450b5b1 100644 --- a/backend/src/v1/team/team.module.ts +++ b/backend/src/v1/team/team.module.ts @@ -5,9 +5,15 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Team } from "./entities/team.entity"; import { CssModule } from "../../external_api/css/css.module"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; +import { Officer } from "../officer/entities/officer.entity"; @Module({ - imports: [TypeOrmModule.forFeature([Team]), TypeOrmModule.forFeature([OfficerTeamXref]), CssModule], + imports: [ + TypeOrmModule.forFeature([Team]), + TypeOrmModule.forFeature([OfficerTeamXref]), + CssModule, + TypeOrmModule.forFeature([Officer]), + ], controllers: [TeamController], providers: [TeamService], exports: [TeamService], diff --git a/backend/src/v1/team/team.service.spec.ts b/backend/src/v1/team/team.service.spec.ts index 1350ce57f..41f59632a 100644 --- a/backend/src/v1/team/team.service.spec.ts +++ b/backend/src/v1/team/team.service.spec.ts @@ -8,6 +8,7 @@ import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref import { CssService } from "../../external_api/css/css.service"; import { ConfigurationService } from "../configuration/configuration.service"; import { Configuration } from "../configuration/entities/configuration.entity"; +import { Officer } from "..//officer/entities/officer.entity"; describe("TeamService", () => { let service: TeamService; @@ -24,6 +25,10 @@ describe("TeamService", () => { provide: getRepositoryToken(OfficerTeamXref), useValue: {}, }, + { + provide: getRepositoryToken(Officer), + useValue: {}, + }, { provide: DataSource, useFactory: dataSourceMockFactory, diff --git a/backend/src/v1/team/team.service.ts b/backend/src/v1/team/team.service.ts index 3d88aef39..2fc43fd54 100644 --- a/backend/src/v1/team/team.service.ts +++ b/backend/src/v1/team/team.service.ts @@ -1,19 +1,26 @@ import { Inject, Injectable, Logger } from "@nestjs/common"; import { Team } from "./entities/team.entity"; -import { Repository } from "typeorm"; +import { DataSource, Repository } from "typeorm"; import { InjectRepository } from "@nestjs/typeorm"; import { CssService } from "../../external_api/css/css.service"; import { OfficerTeamXref } from "../officer_team_xref/entities/officer_team_xref.entity"; import { Role } from "../../enum/role.enum"; +import { TeamUpdate } from "../../types/models/general/team-update"; +import { Officer } from "../officer/entities/officer.entity"; @Injectable() export class TeamService { private readonly logger = new Logger(TeamService.name); + constructor(private readonly dataSource: DataSource) {} + @InjectRepository(Team) - private teamRepository: Repository; + private readonly teamRepository: Repository; @InjectRepository(OfficerTeamXref) - private officerTeamXrefRepository: Repository; + private readonly officerTeamXrefRepository: Repository; + @InjectRepository(Officer) + private readonly officerRepository: Repository; + @Inject(CssService) private readonly cssService: CssService; @@ -61,26 +68,18 @@ export class TeamService { } async findUserCurrentTeam(officerGuid) { - const officerTeamXref = await this.officerTeamXrefRepository.findOne({ + return await this.officerTeamXrefRepository.findOne({ where: { officer_guid: officerGuid }, relations: { team_guid: { team_code: true } }, }); - if (officerTeamXref) { - return { - value: officerTeamXref.team_guid.team_code.team_code, - label: officerTeamXref.team_guid.team_code.short_description, - }; - } else { - return null; - } } - async update(officerGuid, updateTeamData) { + async update(officerGuid, updateTeamData: TeamUpdate) { let result = { team: false, roles: false, }; - const { teamCode, agencyCode, userIdir, roles: updateRoles } = updateTeamData; + const { teamCode, agencyCode, userIdir, roles: updateRoles, adminIdirUsername } = updateTeamData; try { //Update team //Assume one officer belong to one team for now @@ -95,11 +94,14 @@ export class TeamService { } } else { const teamGuid = await this.findByTeamCodeAndAgencyCode(teamCode, agencyCode); + //set user's office_guid to null because CEEB user doesn't have an office + await this.officerRepository.update({ officer_guid: officerGuid }, { office_guid: null }); + if (officerTeamXref) { const updateEnity = { team_guid: teamGuid, active_ind: true, - update_user_id: "postgres", + update_user_id: adminIdirUsername, }; const updateResult = await this.officerTeamXrefRepository.update({ officer_guid: officerGuid }, updateEnity); if (updateResult.affected > 0) { @@ -111,10 +113,10 @@ export class TeamService { officer_guid: officerGuid, team_guid: teamGuid, active_ind: true, - create_user_id: "postgres", - update_user_id: "postgres", + create_user_id: adminIdirUsername, + update_user_id: adminIdirUsername, }; - const newRecord = await this.officerTeamXrefRepository.create(newEntity); + const newRecord = this.officerTeamXrefRepository.create(newEntity); const saveResult = await this.officerTeamXrefRepository.save(newRecord); if (saveResult.officer_guid) { result.team = true; @@ -126,8 +128,8 @@ export class TeamService { const currentRoles: any = await this.cssService.getUserRoles(userIdir); for await (const roleItem of currentRoles) { const rolesMatchWithUpdate = updateRoles.some((updateRole) => updateRole.name === roleItem.name); - //Remove existing roles that do not match with updated roles, but still keeps Admin and Read only role - if (roleItem.name !== Role.TEMPORARY_TEST_ADMIN && roleItem.name !== Role.READ_ONLY && !rolesMatchWithUpdate) { + //Remove existing roles that do not match with updated roles, but still keeps Admin role + if (roleItem.name !== Role.TEMPORARY_TEST_ADMIN && !rolesMatchWithUpdate) { await this.cssService.deleteUserRole(userIdir, roleItem.name); } } diff --git a/backend/test/mocks/personRepositoryMockFactory.ts b/backend/test/mocks/personRepositoryMockFactory.ts index e5094a98e..d75f90a2f 100644 --- a/backend/test/mocks/personRepositoryMockFactory.ts +++ b/backend/test/mocks/personRepositoryMockFactory.ts @@ -47,14 +47,14 @@ export const PersonRepositoryMockFactory = () => ({ }), create: jest .fn() - .mockResolvedValueOnce({ + .mockReturnValueOnce({ person_guid: "8a5131e6-1dd0-484d-8cdc-78ba205dfac8", first_name: "Miss", middle_name_1: "Jane", middle_name_2: null, last_name: "Marple", }) - .mockRejectedValueOnce(new Error("Simulated error")), + .mockReturnValueOnce(new Error("Simulated error")), // as these do not actually use their return values in our tests // we just make sure that their resolve is true to not crash save: jest.fn(() => { diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index b7bd324ed..04ca489cd 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -16,7 +16,7 @@ import { ComplaintsWrapper } from "./components/containers/complaints/complaints import COMPLAINT_TYPES from "./types/app/complaint-types"; import { getCodeTableVersion, getConfigurations, getFeatureFlag, getOfficerDefaultZone } from "./store/reducers/app"; import { CreateComplaint } from "./components/containers/complaints/details/complaint-details-create"; -import { UserManagement } from "./components/containers/admin/user-management"; +import { UserManagement } from "@components/containers/admin/user-management"; import UserService from "./service/user-service"; import GenericErrorBoundary from "./components/error-handling/generic-error-boundary"; import { VerifyAccess } from "./components/containers/pages/verify-access"; diff --git a/frontend/src/app/common/validation-multiselect.tsx b/frontend/src/app/common/validation-multiselect.tsx index 1b4714289..b1106ad90 100644 --- a/frontend/src/app/common/validation-multiselect.tsx +++ b/frontend/src/app/common/validation-multiselect.tsx @@ -12,6 +12,7 @@ interface ValidationMultiSelectProps { onChange: Function; errMsg: string; values?: Option[]; + isDisabled?: boolean; } export const ValidationMultiSelect: FC = ({ @@ -24,6 +25,7 @@ export const ValidationMultiSelect: FC = ({ onChange, errMsg, values, + isDisabled = false, }) => { const errClass = errMsg === "" ? "" : "error-message"; const calulatedClass = errMsg === "" ? "" : "error-border"; @@ -42,6 +44,7 @@ export const ValidationMultiSelect: FC = ({ defaultValue={defaultValue} value={values} isMulti + isDisabled={isDisabled} />
{errMsg}
diff --git a/frontend/src/app/components/containers/admin/user-management.tsx b/frontend/src/app/components/containers/admin/user-management.tsx deleted file mode 100644 index ec5786333..000000000 --- a/frontend/src/app/components/containers/admin/user-management.tsx +++ /dev/null @@ -1,437 +0,0 @@ -import { FC, useEffect, useState } from "react"; -import { Button } from "react-bootstrap"; -import { useAppDispatch, useAppSelector } from "@hooks/hooks"; -import { assignOfficerToOffice, selectOfficersDropdown } from "@store/reducers/officer"; -import { CompSelect } from "@components/common/comp-select"; -import Option from "@apptypes/app/option"; -import { fetchOfficeAssignments, selectOfficesForAssignmentDropdown, selectOffices } from "@store/reducers/office"; -import { ToastContainer } from "react-toastify"; -import { ToggleError, ToggleSuccess } from "@common/toast"; -import { clearNotification, selectNotification } from "@store/reducers/app"; -import { selectAgencyDropdown, selectTeamDropdown } from "@store/reducers/code-table"; -import { CEEB_ROLE_OPTIONS } from "@constants/ceeb-roles"; -import { generateApiParameters, get, patch } from "@common/api"; -import config from "@/config"; -import { Officer } from "@apptypes/person/person"; -import { UUID } from "crypto"; -import { ValidationMultiSelect } from "@common/validation-multiselect"; - -export const UserManagement: FC = () => { - const dispatch = useAppDispatch(); - const officers = useAppSelector(selectOfficersDropdown(true)); - const officeAssignments = useAppSelector(selectOfficesForAssignmentDropdown); - const notification = useAppSelector(selectNotification); - const teams = useAppSelector(selectTeamDropdown); - const agency = useAppSelector(selectAgencyDropdown); - - const availableOffices = useAppSelector(selectOffices); - - const [officer, setOfficer] = useState