Skip to content

Commit

Permalink
Merge branch 'main' into feat/LBAC-1680-webhook-brevo-avec-clef
Browse files Browse the repository at this point in the history
  • Loading branch information
alanlr authored Dec 18, 2023
2 parents dfc1648 + f4146c6 commit 383ce2a
Show file tree
Hide file tree
Showing 22 changed files with 156 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fileignoreconfig:
- filename: server/tests/unit/security/authorisationService.test.ts
checksum: 581074420be582973bbfcdfafe1f700ca32f56e331911609cdc1cb2fb2626383
- filename: server/tests/unit/util.test.ts
checksum: f248ec16629759106d43b02aafc092d55316aeb147dd75fa44f4acc990b4ece0
checksum: cd74a4c09c7865eca459dabcc5d58b075ee6e0d10b97104a83ebca9fe643733c
- filename: shared/constants/recruteur.ts
checksum: 28af032d2eb26aec7dd3bb1d32253f992a036626c36a92eb1e7ff07599fd0b2b
- filename: shared/helpers/generateUri.ts
Expand Down
5 changes: 0 additions & 5 deletions server/src/common/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ export const readXLSXFile = (localPath) => {
return { sheet_name_list: workbook.SheetNames, workbook }
}

export const prepareMessageForMail = (data) => {
const result = data ? data.replace(/(<([^>]+)>)/gi, "") : data
return result ? result.replace(/\r\n|\r|\n/gi, "<br />") : result
}

export const parseCsv = (options: CsvParseOptions = {}) => {
return parse({
trim: true,
Expand Down
7 changes: 4 additions & 3 deletions server/src/http/routes/appointmentRequest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
import { getReferrerByKeyName } from "../../common/model/constants/referrers"
import { Appointment, EligibleTrainingsForAppointment, Etablissement, User } from "../../common/model/index"
import config from "../../config"
import { createRdvaShortRecapToken } from "../../services/appLinks.service"
import * as appointmentService from "../../services/appointment.service"
import { sendCandidateAppointmentEmail, sendFormateurAppointmentEmail } from "../../services/appointment.service"
import dayjs from "../../services/dayjs.service"
Expand Down Expand Up @@ -103,11 +104,10 @@ export default (server: Server) => {
await sendFormateurAppointmentEmail(user, createdAppointement, eligibleTrainingsForAppointment, referrerObj, etablissement)
await sendCandidateAppointmentEmail(user, createdAppointement, eligibleTrainingsForAppointment, referrerObj)

const appointmentUpdated = await Appointment.findById(createdAppointement._id).lean()

res.status(200).send({
userId: user._id,
appointment: appointmentUpdated,
appointment: createdAppointement,
token: createRdvaShortRecapToken(user.email, createdAppointement._id.toString()),
})
}
)
Expand All @@ -116,6 +116,7 @@ export default (server: Server) => {
"/appointment-request/context/short-recap",
{
schema: zRoutes.get["/appointment-request/context/short-recap"],
onRequest: server.auth(zRoutes.get["/appointment-request/context/short-recap"]),
},
async (req, res) => {
const { appointmentId } = req.query
Expand Down
5 changes: 3 additions & 2 deletions server/src/http/routes/auth/login.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Boom from "boom"
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
import { removeUrlsFromText } from "shared/helpers/common"
import { toPublicUser, zRoutes } from "shared/index"

import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
Expand Down Expand Up @@ -84,8 +85,8 @@ export default (server: Server) => {
images: {
logoLba: `${config.publicUrl}/images/emails/logo_LBA.png?raw=true`,
},
last_name,
first_name,
last_name: removeUrlsFromText(last_name),
first_name: removeUrlsFromText(first_name),
connexion_url: createAuthMagicLink(user),
},
})
Expand Down
1 change: 1 addition & 0 deletions server/src/security/accessTokenService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type IAccessToken<Schema extends SchemaWithSecurity = SchemaWithSecurity>
siret: string
}
| { type: "lba-company"; siret: string; email: string }
| { type: "candidat"; email: string }
scopes: ReadonlyArray<IScope<Schema>>
}

Expand Down
2 changes: 1 addition & 1 deletion server/src/security/authorisationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ export function isAuthorized(access: AccessPermission, userWithType: NonTokenUse

switch (access) {
case "recruiter:manage":
case "recruiter:validate":
case "recruiter:add_job":
return resources.recruiters.every((recruiter) => canAccessRecruiter(userWithType, recruiter))

Expand All @@ -325,6 +324,7 @@ export function isAuthorized(access: AccessPermission, userWithType: NonTokenUse
return resources.users.every((user) => canAccessUser(userWithType, user))
case "application:manage":
return resources.applications.every((application) => canAccessApplication(userWithType, application))
case "user:validate":
case "user:manage":
return resources.users.every((user) => canAccessUser(userWithType, user))
case "admin":
Expand Down
16 changes: 16 additions & 0 deletions server/src/services/appLinks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,22 @@ export function createRdvaAppointmentIdPageLink(email: string, siret: string, et
return `${config.publicUrl}/espace-pro/establishment/${etablissementId}/appointments/${appointmentId}?token=${encodeURIComponent(token)}`
}

export function createRdvaShortRecapToken(email: string, appointmentId: string) {
const token = generateAccessToken({ email, type: "candidat" }, [
generateScope({
schema: zRoutes.get["/appointment-request/context/short-recap"],
options: {
params: undefined,
querystring: {
appointmentId,
},
},
}),
])

return token
}

/**
* Secured link for application replies
*/
Expand Down
10 changes: 6 additions & 4 deletions server/src/services/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { oleoduc, writeData } from "oleoduc"
import { IApplication, IApplicationUI, ILbaCompany, JOB_STATUS, ZApplication } from "shared"
import { ApplicantIntention } from "shared/constants/application.js"
import { RECRUITER_STATUS } from "shared/constants/recruteur.js"
import { disableUrlsWith0WidthChar, prepareMessageForMail, removeUrlsFromText } from "shared/helpers/common.js"

import { getStaticFilePath } from "@/common/utils/getStaticFilePath"

import { logger } from "../common/logger.js"
import { Application, EmailBlacklist, LbaCompany, Recruiter, UserRecruteur } from "../common/model/index.js"
import { decryptWithIV } from "../common/utils/encryptString.js"
import { manageApiError } from "../common/utils/errorManager.js"
import { prepareMessageForMail } from "../common/utils/fileUtils.js"
import { sentryCaptureException } from "../common/utils/sentryUtils.js"
import config from "../config.js"

Expand Down Expand Up @@ -330,6 +330,8 @@ const buildRecruiterEmailUrls = async (application: IApplication) => {
const initApplication = (params: Omit<IApplicationUI, "_id">, company_email: string): EnforceDocument<IApplication, any> => {
const res = new Application({
...params,
applicant_first_name: disableUrlsWith0WidthChar(params.applicant_first_name),
applicant_last_name: disableUrlsWith0WidthChar(params.applicant_last_name),
applicant_attachment_name: params.applicant_file_name,
applicant_email: params.applicant_email.toLowerCase(),
applicant_message_to_company: prepareMessageForMail(params.message),
Expand Down Expand Up @@ -534,7 +536,7 @@ export const sendMailToApplicant = async ({
to: application.applicant_email,
subject: `Réponse positive de ${application.company_name}`,
template: getEmailTemplate("mail-candidat-entretien"),
data: { ...application, ...images, email, phone, comment: company_feedback },
data: { ...application, ...images, email, phone: removeUrlsFromText(phone), comment: disableUrlsWith0WidthChar(company_feedback) },
})
break
}
Expand All @@ -543,7 +545,7 @@ export const sendMailToApplicant = async ({
to: application.applicant_email,
subject: `Réponse de ${application.company_name}`,
template: getEmailTemplate("mail-candidat-nsp"),
data: { ...application, ...images, email, phone, comment: company_feedback },
data: { ...application, ...images, email, phone: removeUrlsFromText(phone), comment: disableUrlsWith0WidthChar(company_feedback) },
})
break
}
Expand All @@ -552,7 +554,7 @@ export const sendMailToApplicant = async ({
to: application.applicant_email,
subject: `Réponse négative de ${application.company_name}`,
template: getEmailTemplate("mail-candidat-refus"),
data: { ...application, ...images, comment: company_feedback },
data: { ...application, ...images, comment: disableUrlsWith0WidthChar(company_feedback) },
})
break
}
Expand Down
30 changes: 15 additions & 15 deletions server/tests/unit/security/authorisationService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ describe("authorisationService", () => {
describe("as an admin user", () => {
describe.each<[Permission]>([
["recruiter:manage"],
["recruiter:validate"],
["user:validate"],
["recruiter:add_job"],
["job:manage"],
["school:manage"],
Expand Down Expand Up @@ -391,7 +391,7 @@ describe("authorisationService", () => {
})

describe("as an opco user", () => {
describe.each<[Permission]>([["recruiter:manage"], ["recruiter:validate"], ["recruiter:add_job"]])("I have %s permission", (permission) => {
describe.each<[Permission]>([["recruiter:manage"], ["user:validate"], ["recruiter:add_job"]])("I have %s permission", (permission) => {
it("on all recruiters from my opco", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO1E1R1, recruteurO1E1R2, recruteurO1E2R1], location)
await expect(
Expand Down Expand Up @@ -454,7 +454,7 @@ describe("authorisationService", () => {
).resolves.toBe(undefined)
})
})
describe.each<[Permission]>([["user:manage"]])("I have %s permission", (permission) => {
describe.each<[Permission]>([["user:manage"], ["user:validate"]])("I have %s permission", (permission) => {
it("on user recruiter from my Opco", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurUserO1E1R1], location)
await expect(
Expand All @@ -476,7 +476,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["recruiter:manage"], ["recruiter:validate"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["recruiter:manage"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
it("on recruiter from other Opco", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO2E1R1], location)
await expect(
Expand Down Expand Up @@ -585,7 +585,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["user:manage"], ["admin"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["user:manage"], ["admin"], ["user:validate"]])("I do not have %s permission", (permission) => {
it("on admin user", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [adminUser], location)
await expect(
Expand All @@ -607,7 +607,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["user:manage"], ["admin"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["user:manage"], ["admin"], ["user:validate"]])("I do not have %s permission", (permission) => {
it("on user CFA", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [cfaUser1], location)
await expect(
Expand All @@ -629,7 +629,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["user:manage"], ["admin"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["user:manage"], ["admin"], ["user:validate"]])("I do not have %s permission", (permission) => {
it("on other user Opco from my Opco", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [opcoUserO1U2], location)
await expect(
Expand All @@ -653,7 +653,7 @@ describe("authorisationService", () => {
})

describe("as an opco credential", () => {
describe.each<[Permission]>([["recruiter:manage"], ["recruiter:validate"], ["recruiter:add_job"]])("I have %s permission", (permission) => {
describe.each<[Permission]>([["recruiter:manage"], ["user:validate"], ["recruiter:add_job"]])("I have %s permission", (permission) => {
it("on all recruiters from my opco", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO1E1R1, recruteurO1E1R2, recruteurO1E2R1], location)
await expect(
Expand Down Expand Up @@ -717,7 +717,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["recruiter:manage"], ["recruiter:validate"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["recruiter:manage"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
it("on recruiter from other Opco", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO2E1R1], location)
await expect(
Expand Down Expand Up @@ -980,7 +980,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["recruiter:validate"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["user:validate"]])("I do not have %s permission", (permission) => {
it("on all my delegated recruiters", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO1E1R1], location)
await expect(
Expand All @@ -1002,7 +1002,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["recruiter:manage"], ["recruiter:validate"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["recruiter:manage"], ["user:validate"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
it("on non delegated recruiters", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO1E1R2], location)
await expect(
Expand Down Expand Up @@ -1242,7 +1242,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["recruiter:validate"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["user:validate"]])("I do not have %s permission", (permission) => {
it("on me", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO1E1R1], location)
await expect(
Expand All @@ -1264,7 +1264,7 @@ describe("authorisationService", () => {
})
})

describe.each<[Permission]>([["recruiter:manage"], ["recruiter:validate"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
describe.each<[Permission]>([["recruiter:manage"], ["user:validate"], ["recruiter:add_job"], ["admin"]])("I do not have %s permission", (permission) => {
it("on other recruiters from my company", async () => {
const [securityScheme, req] = generateSecuritySchemeFixture(permission, [recruteurO1E1R2], location)
await expect(
Expand Down Expand Up @@ -1479,7 +1479,7 @@ describe("authorisationService", () => {
it("should support some operator permission", async () => {
const securityScheme: SecurityScheme = {
auth: "cookie-session",
access: { some: ["recruiter:manage", "recruiter:validate"] },
access: { some: ["recruiter:manage", "user:validate"] },
resources: {
recruiter: [
{
Expand Down Expand Up @@ -1535,7 +1535,7 @@ describe("authorisationService", () => {
it("should support every operator permission", async () => {
const securityScheme: SecurityScheme = {
auth: "cookie-session",
access: { every: ["recruiter:manage", "recruiter:validate"] },
access: { every: ["recruiter:manage", "user:validate"] },
resources: {
recruiter: [
{
Expand Down
29 changes: 28 additions & 1 deletion server/tests/unit/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "assert"

import { cleanEmail } from "shared/helpers/common"
import { cleanEmail, removeUrlsFromText, disableUrlsWith0WidthChar } from "shared/helpers/common"
import { describe, it } from "vitest"

import __filename from "../../src/common/filename"
Expand Down Expand Up @@ -39,6 +39,33 @@ describe(__filename(import.meta.url), () => {
assert.strictEqual(cleanEmail("jhönœ.dôœ.’£'^&/=!*?}ù@têst .com "), "[email protected]")
})

it("Suppression des différentes formes d'URL dans un texte", () => {
assert.strictEqual(removeUrlsFromText(undefined), "")
assert.strictEqual(removeUrlsFromText(null), "")
assert.strictEqual(removeUrlsFromText(""), "")
assert.strictEqual(removeUrlsFromText("clean text"), "clean text")
assert.strictEqual(removeUrlsFromText("text https://url.com end"), "text end")
assert.strictEqual(removeUrlsFromText("text http://www.url.com https://url.com [email protected] end"), "text end")
assert.strictEqual(removeUrlsFromText("text https://url.com www.url.com/?meh=lah mailto:[email protected] ftp://bad-ressource.com/path/path"), "text ")
})

it("Mise entre [] des différentes formes d'URL dans un texte", () => {
assert.strictEqual(disableUrlsWith0WidthChar(undefined), "")
assert.strictEqual(disableUrlsWith0WidthChar(null), "")
assert.strictEqual(disableUrlsWith0WidthChar(""), "")
assert.strictEqual(disableUrlsWith0WidthChar("clean text"), "clean text")
assert.strictEqual(disableUrlsWith0WidthChar("clean [email protected] text"), "clean evil-pirate@hack\u200B.\u200Bcom text")
assert.strictEqual(disableUrlsWith0WidthChar("text https://url.com end"), "text https://url\u200B.\u200Bcom end")
assert.strictEqual(
disableUrlsWith0WidthChar("text http://www.url.com https://url.com [email protected] end"),
"text http://www\u200B.\u200Burl\u200B.\u200Bcom https://url\u200B.\u200Bcom evil-pirate@hack\u200B.\u200Bcom end"
)
assert.strictEqual(
disableUrlsWith0WidthChar("text https://url.com www.url.com/?meh=lah mailto:[email protected] ftp://bad-ressource.com/path/path"),
"text https://url\u200B.\u200Bcom www\u200B.\u200Burl\u200B.\u200Bcom/?meh=lah mailto:evil@hack\u200B.\u200Bcom ftp://bad-ressource\u200B.\u200Bcom/path/path"
)
})

it.skip("Encryption décryption fonctionne", () => {
const value = "Chaîne@crypter"

Expand Down
19 changes: 19 additions & 0 deletions shared/helpers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,22 @@ export const cleanEmail = (email: string) => {
cleanedEmail = cleanedEmail.replace(new RegExp("œ", "gi"), "o")
return cleanedEmail
}

const linkRegexes = [/\b(https?:\/\/[^\s]+\b)/g, /\bwww\.[^\s]+\b/g, /\bmailto:([^\s<>]+)\b/g, /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, /\bftp:\/\/[^\s]+\b/g]

export const removeUrlsFromText = (text: string | null | undefined) => {
if (!text) return ""
return linkRegexes.reduce((processedText, regex) => processedText.replace(regex, ""), text)
}

export const disableUrlsWith0WidthChar = (text: string | null | undefined) => {
if (!text) return ""
return linkRegexes.reduce((processedText, regex) => processedText.replace(regex, (url) => url.replace(/\./g, "\u200B.\u200B")), text)
}

export const prepareMessageForMail = (text: string | null | undefined) => {
if (!text) return ""
let result: string = text.replace(/(<([^>]+)>)/gi, "")
result = disableUrlsWith0WidthChar(result)
return result ? result.replace(/\r\n|\r|\n/gi, "<br />") : result
}
7 changes: 6 additions & 1 deletion shared/helpers/zodHelpers/zodPrimitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { capitalize } from "lodash-es"

import { CODE_NAF_REGEX, SIRET_REGEX, UAI_REGEX } from "../../constants/regex"
import { validateSIRET } from "../../validators/siretValidator"
import { removeUrlsFromText } from "../common"
import { z } from "../zodWithOpenApi"

// custom error map to translate zod errors to french
Expand Down Expand Up @@ -34,7 +35,11 @@ export const extensions = {
example: "78424186100011",
}),
uai: () => z.string().trim().regex(UAI_REGEX, "UAI invalide"), // e.g 0123456B
phone: () => z.string().trim() /*.regex(phoneRegex)*/,
phone: () =>
z
.string()
.trim()
.transform((value) => removeUrlsFromText(value)) /*.regex(phoneRegex)*/,
code_naf: () =>
z.preprocess(
(v: unknown) => (typeof v === "string" ? v.replace(".", "") : v), // parfois, le code naf contient un point
Expand Down
Loading

0 comments on commit 383ce2a

Please sign in to comment.