Skip to content

Commit

Permalink
feat: apply securisation back and front
Browse files Browse the repository at this point in the history
  • Loading branch information
kevbarns committed Dec 13, 2023
1 parent e0b1025 commit 15bc3ed
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 133 deletions.
43 changes: 17 additions & 26 deletions server/src/http/routes/sendApplication.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { zRoutes } from "shared/index"
import config from "@/config"

import { Application } from "../../common/model/index"
import { decryptWithIV } from "../../common/utils/encryptString"
import { sentryCaptureException } from "../../common/utils/sentryUtils"
import { sendMailToApplicant, updateApplicationStatus, validateFeedbackApplicationComment } from "../../services/application.service"
import { sendMailToApplicant, updateApplicationStatus } from "../../services/application.service"
import { Server } from "../server"

const rateLimitConfig = {
Expand All @@ -19,59 +18,51 @@ const rateLimitConfig = {

export default function (server: Server) {
server.post(
"/application/intentionComment",
"/application/intentionComment/:id",
{
schema: zRoutes.post["/application/intentionComment"],
schema: zRoutes.post["/application/intentionComment/:id"],
onRequest: server.auth(zRoutes.post["/application/intentionComment/:id"]),
config: rateLimitConfig,
},
async (req, res) => {
// email and phone should appear
await validateFeedbackApplicationComment({
id: req.body.id,
iv: req.body.iv,
comment: req.body.comment,
})

const decryptedId = decryptWithIV(req.body.id, req.body.iv)
const { id } = req.params
const { company_recruitment_intention, company_feedback } = req.body

try {
const application = await Application.findOneAndUpdate(
{ _id: new mongoose.Types.ObjectId(decryptedId) },
{ company_recruitment_intention: req.body.intention, company_feedback: req.body.comment, company_feedback_date: new Date() }
{ _id: new mongoose.Types.ObjectId(id) },
{ company_recruitment_intention, company_feedback, company_feedback_date: new Date() }
)
if (!application) throw new Error("application not found")

await sendMailToApplicant({
application,
intention: req.body.intention,
intention: company_recruitment_intention,
email: req.body.email,
phone: req.body.phone,
comment: req.body.comment,
comment: company_feedback,
})

return res.status(200).send({ result: "ok", message: "comment registered" })
} catch (err) {
console.error("err ", err)
sentryCaptureException(err)
// TODO: return 500
return res.status(200).send({ error: "error_saving_comment" })
throw Boom.badRequest("error_saving_comment")
}
}
)

server.post(
"/application/intention",
"/application/intention/:id",
{
schema: zRoutes.post["/application/intention"],
schema: zRoutes.post["/application/intention/:id"],
onRequest: server.auth(zRoutes.post["/application/intention/:id"]),
config: rateLimitConfig,
},
async (req, res) => {
const decryptedId = decryptWithIV(req.body.id, req.body.iv)
const { id } = req.params
const { company_recruitment_intention } = req.body

const application = await Application.findOneAndUpdate(
{ _id: new mongoose.Types.ObjectId(decryptedId) },
{ company_recruitment_intention: req.body.intention, company_feedback_date: new Date() }
)
const application = await Application.findOneAndUpdate({ _id: new mongoose.Types.ObjectId(id) }, { company_recruitment_intention, company_feedback_date: new Date() })
if (!application) throw new Error("application not found")

return res.status(200).send({ result: "ok" })
Expand Down
12 changes: 8 additions & 4 deletions server/src/services/appLinks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,16 @@ export const createLbaCompanyApplicationReplyLink = async (email, siret, intenti
generateScope({
schema: zRoutes.post["/application/intention"],
options: {
params: undefined,
params: { id: application._id },
querystring: undefined,
},
resources: {},
}),
])

return `${config.publicUrl}/formulaire-intention?intention=${intention}${candidateData}${utmRecruiterData}&token=${encodeURIComponent(token)}`
return `${config.publicUrl}/formulaire-intention?company_recruitment_intention=${intention}&id=${application.id}${candidateData}${utmRecruiterData}&token=${encodeURIComponent(
token
)}`
}

export const createUserRecruteurApplicationReplyLink = async (user, intention, application) => {
Expand All @@ -337,12 +339,14 @@ export const createUserRecruteurApplicationReplyLink = async (user, intention, a
generateScope({
schema: zRoutes.post["/application/intention"],
options: {
params: undefined,
params: { id: application._id },
querystring: undefined,
},
resources: {},
}),
])

return `${config.publicUrl}/formulaire-intention?intention=${intention}${candidateData}${utmRecruiterData}&token=${encodeURIComponent(token)}`
return `${config.publicUrl}/formulaire-intention?company_recruitment_intention=${intention}&id=${application.id}${candidateData}${utmRecruiterData}&token=${encodeURIComponent(
token
)}`
}
46 changes: 23 additions & 23 deletions server/src/services/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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, encryptIdWithIV } from "../common/utils/encryptString.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"
Expand Down Expand Up @@ -181,19 +181,13 @@ export const sendApplication = async ({
try {
const application = initApplication(query, company_email)

const encryptedId = encryptIdWithIV(application.id)

const emailTemplates = getEmailTemplates(query.company_type)

const fileContent = query.applicant_file_content

const urlOfDetail = buildUrlOfDetail(publicUrl, query)
const urlOfDetailNoUtm = urlOfDetail.replace(/(?<=&|\?)utm_.*?(&|$)/gim, "")
const recruiterEmailUrls = await buildRecruiterEmailUrls({
publicUrl,
application,
encryptedId,
})
const recruiterEmailUrls = await buildRecruiterEmailUrls(application)

const searched_for_job_label = query.searched_for_job_label || ""

Expand All @@ -213,7 +207,7 @@ export const sendApplication = async ({
to: application.applicant_email,
subject: `Votre candidature chez ${application.company_name}`,
template: getEmailTemplate(emailTemplates.candidat),
data: { ...application.toObject(), ...images, ...encryptedId, publicUrl, urlOfDetail, urlOfDetailNoUtm },
data: { ...application.toObject(), ...images, publicUrl, urlOfDetail, urlOfDetailNoUtm },
attachments: [
{
filename: application.applicant_attachment_name,
Expand Down Expand Up @@ -290,10 +284,8 @@ const buildUrlOfDetail = (publicUrl: string, query: Pick<IApplicationUI, "job_id
/**
* Build urls to add in email messages sent to the recruiter
*/
const buildRecruiterEmailUrls = async ({ publicUrl, application, encryptedId }: { publicUrl: string; application: EnforceDocument<IApplication, any>; encryptedId: any }) => {
const buildRecruiterEmailUrls = async (application: IApplication) => {
const utmRecruiterData = "&utm_source=jecandidate&utm_medium=email&utm_campaign=jecandidaterecruteur"
const candidateData = `&fn=${application.toObject().applicant_first_name}&ln=${application.toObject().applicant_last_name}`
const encryptedData = `&id=${encryptedId.id}&iv=${encryptedId.iv}`

// get the related recruiters to fetch it's establishment_id
const recruiter = await Recruiter.findOne({ "jobs._id": application.job_id }).lean()
Expand All @@ -303,22 +295,30 @@ const buildRecruiterEmailUrls = async ({ publicUrl, application, encryptedId }:
userRecruteur = await UserRecruteur.findOne({ establishment_id: recruiter.establishment_id }).lean()
}

//Offre matcha - IUserRecruteur
//Offre LBA -- email & siret

const urls = {
meetCandidateUrl:
application.job_origin === "lba"
? createLbaCompanyApplicationReplyLink(application.company_siret, application.company_email, ApplicantIntention.ENTRETIEN, application)
: createUserRecruteurApplicationReplyLink(userRecruteur, ApplicantIntention.ENTRETIEN, application),
waitCandidateUrl: `${publicUrl}/formulaire-intention?intention=ne_sais_pas${encryptedData}${candidateData}${utmRecruiterData}`,
refuseCandidateUrl: `${publicUrl}/formulaire-intention?intention=refus${encryptedData}${candidateData}${utmRecruiterData}`,
lbaRecruiterUrl: `${publicUrl}/acces-recruteur?${utmRecruiterData}`,
unsubscribeUrl: `${publicUrl}/desinscription?email=${application.company_email}${utmRecruiterData}`,
lbaUrl: `${publicUrl}?${utmRecruiterData}`,
jobProvidedUrl: createProvidedJobLink(userRecruteur, application.job_id, utmRecruiterData),
cancelJobUrl: createCancelJobLink(userRecruteur, application.job_id, utmRecruiterData),
faqUrl: `${publicUrl}/faq?${utmRecruiterData}`,
waitCandidateUrl:
application.job_origin === "lba"
? createLbaCompanyApplicationReplyLink(application.company_siret, application.company_email, ApplicantIntention.NESAISPAS, application)
: createUserRecruteurApplicationReplyLink(userRecruteur, ApplicantIntention.NESAISPAS, application),
refuseCandidateUrl:
application.job_origin === "lba"
? createLbaCompanyApplicationReplyLink(application.company_siret, application.company_email, ApplicantIntention.REFUS, application)
: createUserRecruteurApplicationReplyLink(userRecruteur, ApplicantIntention.REFUS, application),
lbaRecruiterUrl: `${config.publicUrl}/acces-recruteur?${utmRecruiterData}`,
unsubscribeUrl: `${config.publicUrl}/desinscription?email=${application.company_email}${utmRecruiterData}`,
lbaUrl: `${config.publicUrl}?${utmRecruiterData}`,
faqUrl: `${config.publicUrl}/faq?${utmRecruiterData}`,
jobProvidedUrl: "",
cancelJobUrl: "",
}

if (application.job_id) {
urls.jobProvidedUrl = createProvidedJobLink(userRecruteur, application.job_id, utmRecruiterData)
urls.cancelJobUrl = createCancelJobLink(userRecruteur, application.job_id, utmRecruiterData)
}

return urls
Expand Down
20 changes: 9 additions & 11 deletions shared/routes/application.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,14 @@ export const zApplicationRoutes = {
"Envoi d'un email de candidature à une offre postée sur La bonne alternance recruteur ou une candidature spontanée à une entreprise identifiée par La bonne alternance.\nL'email est envoyé depuis l'adresse générique \"Ne pas répondre\" de La bonne alternance.\n",
},
},
"/application/intention": {
"/application/intention/:id": {
// TODO_SECURITY_FIX
path: "/application/intention",
path: "/application/intention/:id",
method: "post",
params: z.object({ id: z.string() }).strict(),
body: z
.object({
id: z.string(), // inutile de chiffrer l'id, rajouter un champ token qui contiendra l'id
iv: z.string(),
intention: z.string(),
company_recruitment_intention: z.string(),
})
.strict(),
response: {
Expand All @@ -59,15 +58,14 @@ export const zApplicationRoutes = {
resources: {},
},
},
"/application/intentionComment": {
path: "/application/intentionComment",
"/application/intentionComment/:id": {
path: "/application/intentionComment/:id",
method: "post",
params: z.object({ id: z.string() }).strict(),
body: z
.object({
id: z.string(),
iv: z.string(),
comment: z.string(),
intention: z.string(),
company_feedback: z.string(),
company_recruitment_intention: z.string(),
email: z.string(),
phone: z.string(),
})
Expand Down
Loading

0 comments on commit 15bc3ed

Please sign in to comment.