Skip to content

Commit

Permalink
fix(backend): add validators to search
Browse files Browse the repository at this point in the history
  • Loading branch information
pYassine committed Jan 27, 2025
1 parent 85152d6 commit e0b0dd9
Show file tree
Hide file tree
Showing 33 changed files with 551 additions and 253 deletions.
12 changes: 2 additions & 10 deletions packages/backend/src/_common/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
//@index('./*.ts', f => `export * from '${f.path}'`)
export * from "./IsValidPasswordDecorator";
export * from "./IsValidPhoneDecorator";
export * from "./LowerCaseDecorator";
export * from "./parse-hard-reset-token.pipe";
export * from "./parse-token.pipe";
export * from "./ParseRegion.pipe";
export * from "./ParseString.pipe";
export * from "./PhoneTransformDecorator";
export * from "./StripTagsDecorator";
export * from "./TrimDecorator";
export * from "./TrimOrNullDecorator";
export * from "./UpperCaseDecorator";
export * from "./transformers";
export * from "./parse-pipes";
5 changes: 5 additions & 0 deletions packages/backend/src/_common/decorators/parse-pipes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//@index('./*.ts', f => `export * from '${f.path}'`)
export * from "./parse-hard-reset-token.pipe";
export * from "./parse-token.pipe";
export * from "./ParseRegion.pipe";
export * from "./ParseString.pipe";
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BadRequestException } from "@nestjs/common";
import { ParseHardResetTokenPipe } from "../parse-hard-reset-token.pipe";
import { ParseHardResetTokenPipe } from "../parse-pipes/parse-hard-reset-token.pipe";

describe("ParseHardResetTokenPipe", () => {
let pipe: ParseHardResetTokenPipe;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from "@nestjs/common";
import { tokenGenerator } from "../../../util";
import { ParseTokenPipe } from "../parse-token.pipe";
import { ParseTokenPipe } from "../parse-pipes/parse-token.pipe";

describe("ParseTokenPipe", () => {
let pipe: ParseTokenPipe;
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/_common/decorators/transformers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//@index('./*.ts', f => `export * from '${f.path}'`)
export * from "./LowerCaseDecorator";
export * from "./PhoneTransformDecorator";
export * from "./StripTagsDecorator";
export * from "./TrimDecorator";
export * from "./TrimOrNullDecorator";
export * from "./UpperCaseDecorator";
4 changes: 4 additions & 0 deletions packages/backend/src/app.bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export async function bootstrapApplication(): Promise<{
stopAtFirstError: true,
enableDebugMessages: true,
disableErrorMessages: domifaConfig().envId !== "local",
transform: true,
transformOptions: {
enableImplicitConversion: false,
},
})
);

Expand Down
212 changes: 212 additions & 0 deletions packages/backend/src/usagers/controllers/search-usagers.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {
Usager,
UsagerDecision,
CriteriaSearchField,
getUsagerDeadlines,
ETAPE_ENTRETIEN,
} from "@domifa/common";
import {
Body,
Controller,
Get,
ParseBoolPipe,
Post,
Query,
UseGuards,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { ApiBearerAuth } from "@nestjs/swagger";
import { format, parse, subMinutes } from "date-fns";
import { Not } from "typeorm";
import {
USER_STRUCTURE_ROLE_ALL,
UserStructureAuthenticated,
} from "../../_common/model";
import { AllowUserStructureRoles, CurrentUser } from "../../auth/decorators";
import { AppUserGuard } from "../../auth/guards";
import {
usagerRepository,
USAGER_LIGHT_ATTRIBUTES,
joinSelectFields,
} from "../../database";

import { SearchUsagerDto } from "../dto";

@Controller("search-usagers")
@UseGuards(AuthGuard("jwt"), AppUserGuard)
@ApiBearerAuth()
export class SearchUsagersController {
@Get()
@AllowUserStructureRoles(...USER_STRUCTURE_ROLE_ALL)
public async findAllByStructure(
@Query("chargerTousRadies", new ParseBoolPipe())
chargerTousRadies: boolean,
@CurrentUser() user: UserStructureAuthenticated
) {
const usagersNonRadies = await usagerRepository.find({
where: {
statut: Not("RADIE"),
structureId: user.structureId,
},
select: USAGER_LIGHT_ATTRIBUTES,
});

const usagersRadiesFirsts = await usagerRepository.find({
where: {
statut: "RADIE",
structureId: user.structureId,
},
select: USAGER_LIGHT_ATTRIBUTES,
take: chargerTousRadies ? undefined : 1600,
});

const usagersRadiesTotalCount = chargerTousRadies
? usagersRadiesFirsts.length
: await usagerRepository.count({
where: {
statut: "RADIE",
structureId: user.structureId,
},
});

const filterHistorique = (usager: Usager) => {
if (usager.historique && Array.isArray(usager.historique)) {
usager.historique = usager.historique.map((item: UsagerDecision) => ({
statut: item.statut,
dateDecision: item.dateDecision,
dateDebut: item.dateDebut,
dateFin: item.dateFin,
})) as UsagerDecision[];
}
return usager;
};

const usagersMerges = [...usagersNonRadies, ...usagersRadiesFirsts].map(
filterHistorique
);

return {
usagersRadiesTotalCount,
usagers: usagersMerges,
};
}

@Get("update-manage")
@AllowUserStructureRoles(...USER_STRUCTURE_ROLE_ALL)
public async updateManage(@CurrentUser() user: UserStructureAuthenticated) {
return await usagerRepository
.createQueryBuilder()
.select(joinSelectFields(USAGER_LIGHT_ATTRIBUTES))
.where(
`"structureId" = :structureId AND "updatedAt" >= :fiveMinutesAgo`,
{
structureId: user.structureId,
fiveMinutesAgo: subMinutes(new Date(), 5),
}
)
.getRawMany();
}

@Post("search-radies")
@AllowUserStructureRoles(...USER_STRUCTURE_ROLE_ALL)
public async searchInRadies(
@Body() search: SearchUsagerDto,
@CurrentUser() user: UserStructureAuthenticated
) {
const query = usagerRepository
.createQueryBuilder("usager")
.select(joinSelectFields(USAGER_LIGHT_ATTRIBUTES))
.where(`"structureId" = :structureId and statut = 'RADIE'`, {
structureId: user.structureId,
});

if (search.searchString) {
if (search.searchStringField === CriteriaSearchField.DEFAULT) {
query.andWhere("nom_prenom_surnom_ref ILIKE :str", {
str: `%${search.searchString}%`,
});
} else if (search.searchStringField === CriteriaSearchField.BIRTH_DATE) {
const formattedDate = format(
parse(search.searchString, "ddMMyyyy", new Date()),
"yyyy-MM-dd"
);
query.andWhere(`DATE("dateNaissance") = DATE(:date)`, {
date: formattedDate,
});
} else if (
search.searchStringField === CriteriaSearchField.PHONE_NUMBER
) {
query.andWhere(`telephone->>'numero' ILIKE :phone`, {
phone: `%${search.searchString}%`,
});
}
}

if (search?.lastInteractionDate) {
const deadlines = getUsagerDeadlines();
const date = deadlines[search.lastInteractionDate].value;

query.andWhere(
`("lastInteraction"->>'dateInteraction')::timestamp >= :dateRef::timestamp`,
{
dateRef: date,
}
);
}

if (typeof search?.referrerId !== "undefined") {
query.andWhere(
search.referrerId === null
? `"referrerId" IS NULL`
: `"referrerId" = :referrerId`,
{ referrerId: search.referrerId }
);
}

if (search?.entretien) {
query.andWhere(
`rdv->>'dateRdv' IS NOT NULL AND "etapeDemande" <= :step AND (rdv->>'dateRdv')::date ${
search.entretien === "COMING" ? ">" : "<"
} CURRENT_DATE`,
{ step: ETAPE_ENTRETIEN }
);
}

if (search?.echeance) {
const deadlines = getUsagerDeadlines();
const now = new Date();
const deadline = deadlines[search.echeance];

if (search.echeance === "EXCEEDED") {
query.andWhere(`(decision->>'dateDecision')::timestamp < :now`, {
now,
});
} else if (search.echeance.startsWith("NEXT_")) {
query.andWhere(
`(decision->>'dateDecision')::timestamp <= :deadline AND (decision->>'dateDecision')::timestamp > :now`,
{
deadline: deadline.value,
now,
}
);
} else if (search?.echeance.startsWith("PREVIOUS_")) {
query.andWhere(`(decision->>'dateDecision')::timestamp < :deadline`, {
deadline: deadline.value,
now,
});
}
}

if (
!search.searchString &&
!search?.echeance &&
!search?.entretien &&
typeof search?.referrerId !== undefined &&
!search?.lastInteractionDate
) {
query.take(100);
}

return await query.getRawMany();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AppTestContext, AppTestHttpClient } from "../../../util/test";
import { USER_STRUCTURE_ROLE_ALL } from "../../../_common/model";
import {
AppTestHttpClientSecurityTestDef,
expectedResponseStatusBuilder,
} from "../../../_tests";

////////////////// IMPORTANT //////////////////
//
// Ce fichier doit être importé dans :
// - API_SECURITY_STRUCTURE_CONTROLLER_TEST_DEFS
//

const CONTROLLER = "SearchUsagersController";

export const UsagersControllerSecurityTests: AppTestHttpClientSecurityTestDef[] =
[
{
label: `${CONTROLLER}.findAllByStructure`,
query: async (context: AppTestContext) => ({
response: await AppTestHttpClient.get(
"/usagers?chargerTousRadies=false",
{
context,
}
),
expectedStatus: expectedResponseStatusBuilder.allowStructureOnly(
context.user,
{
roles: USER_STRUCTURE_ROLE_ALL,
}
),
}),
},
];
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { HttpStatus } from "@nestjs/common";
import { AppTestContext, AppTestHttpClient } from "../../../util/test";
import { USER_STRUCTURE_ROLE_ALL } from "../../../_common/model";
import {
AppTestHttpClientSecurityTestDef,
expectedResponseStatusBuilder,
Expand All @@ -16,23 +15,6 @@ const CONTROLLER = "UsagersController";

export const UsagersControllerSecurityTests: AppTestHttpClientSecurityTestDef[] =
[
{
label: `${CONTROLLER}.findAllByStructure`,
query: async (context: AppTestContext) => ({
response: await AppTestHttpClient.get(
"/usagers?chargerTousRadies=false",
{
context,
}
),
expectedStatus: expectedResponseStatusBuilder.allowStructureOnly(
context.user,
{
roles: USER_STRUCTURE_ROLE_ALL,
}
),
}),
},
{
label: `${CONTROLLER}.checkDuplicates`,
query: async (context: AppTestContext) => ({
Expand Down
Loading

0 comments on commit e0b0dd9

Please sign in to comment.