diff --git a/.talismanrc b/.talismanrc
index 9cb8bad726..b126e61263 100644
--- a/.talismanrc
+++ b/.talismanrc
@@ -61,12 +61,16 @@ fileignoreconfig:
checksum: f77ba0fa55f1cbd5b708fdc9f0d5b229e84a1de6c433186d96aa94a7f6388f0c
- filename: server/tests/unit/security/accessTokenService.test.ts
checksum: 232b1bae52a4d4f961637f59b09da5e480147864c76980a2b86e77a73bb36923
+- filename: server/tests/unit/security/accessTokenService.test.ts
+ checksum: 58153e7e57ef450bfbf061f322cd8b7643dba12513b183194b4eafdb166f58d3
- filename: server/tests/unit/security/authorisationService.test.ts
checksum: 581074420be582973bbfcdfafe1f700ca32f56e331911609cdc1cb2fb2626383
- filename: shared/constants/recruteur.ts
checksum: 28af032d2eb26aec7dd3bb1d32253f992a036626c36a92eb1e7ff07599fd0b2b
- filename: shared/helpers/generateUri.ts
checksum: 03ecb8627c19374e97450e5974ca6a5f51e269a8bb1cf5d372a8c2a2aca72cfa
+- filename: shared/helpers/generateUri.ts
+ checksum: 6542db0d3eca959c6e81d574f8b71d4b18d2f1af21042ca5ed4dff319cd39555
- filename: shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap
checksum: 9358c7f8155efcfc5687be3f01ae3516a05988118304c124a3358094aa966363
- filename: shared/helpers/openapi/generateOpenapi.test.ts
diff --git a/server/src/http/routes/appointmentRequest.controller.ts b/server/src/http/routes/appointmentRequest.controller.ts
index fdd5a5f878..07d2b64cfa 100644
--- a/server/src/http/routes/appointmentRequest.controller.ts
+++ b/server/src/http/routes/appointmentRequest.controller.ts
@@ -3,6 +3,7 @@ import Joi from "joi"
import { zRoutes } from "shared/index"
import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
+import { createRdvaAppointmentIdPageLink } from "@/services/appLinks.service"
import { mailType } from "../../common/model/constants/appointments"
import { getReferrerByKeyName } from "../../common/model/constants/referrers"
@@ -95,6 +96,10 @@ export default (server: Server) => {
}),
])
+ if (!etablissement?.formateur_siret) {
+ throw new Error("Etablissement formateur_siret not found")
+ }
+
const mailData = {
appointmentId: createdAppointement._id,
user: {
@@ -118,7 +123,6 @@ export default (server: Server) => {
reasons: createdAppointement.applicant_reasons,
referrerLink: referrerObj.url,
appointment_origin: referrerObj.full_name,
- link: `${config.publicUrl}/espace-pro/establishment/${etablissement?._id}/appointments/${createdAppointement._id}?utm_source=mail`,
},
images: {
logoLba: `${config.publicUrl}/images/emails/logo_LBA.png?raw=true`,
@@ -142,11 +146,13 @@ export default (server: Server) => {
data: mailData,
}),
mailer.sendEmail({
- // TODO to check string | null
to: eligibleTrainingsForAppointment.lieu_formation_email,
subject: emailCfaSubject,
template: getStaticFilePath("./templates/mail-cfa-demande-de-contact.mjml.ejs"),
- data: mailData,
+ data: {
+ ...mailData,
+ link: createRdvaAppointmentIdPageLink(eligibleTrainingsForAppointment.lieu_formation_email, etablissement.formateur_siret, etablissement._id, createdAppointement._id),
+ },
}),
])
@@ -189,10 +195,58 @@ export default (server: Server) => {
}
)
+ server.get(
+ "/appointment-request/context/short-recap",
+ {
+ schema: zRoutes.get["/appointment-request/context/short-recap"],
+ },
+ async (req, res) => {
+ const { appointmentId } = req.query
+
+ const appointment = await Appointment.findById(appointmentId, { cle_ministere_educatif: 1, applicant_id: 1 }).lean()
+
+ if (!appointment) {
+ throw Boom.notFound()
+ }
+
+ const [etablissement, user] = await Promise.all([
+ EligibleTrainingsForAppointment.findOne(
+ { cle_ministere_educatif: appointment.cle_ministere_educatif },
+ {
+ etablissement_formateur_raison_sociale: 1,
+ lieu_formation_email: 1,
+ _id: 0,
+ }
+ ).lean(),
+ User.findById(appointment.applicant_id, {
+ lastname: 1,
+ firstname: 1,
+ phone: 1,
+ email: 1,
+ _id: 0,
+ }).lean(),
+ ])
+
+ if (!etablissement) {
+ throw Boom.internal("Etablissment not found")
+ }
+
+ if (!user) {
+ throw Boom.internal("User not found")
+ }
+
+ res.status(200).send({
+ user,
+ etablissement,
+ })
+ }
+ )
+
server.get(
"/appointment-request/context/recap",
{
schema: zRoutes.get["/appointment-request/context/recap"],
+ onRequest: [server.auth(zRoutes.get["/appointment-request/context/recap"])],
},
async (req, res) => {
const { appointmentId } = req.query
@@ -252,6 +306,7 @@ export default (server: Server) => {
"/appointment-request/reply",
{
schema: zRoutes.post["/appointment-request/reply"],
+ onRequest: [server.auth(zRoutes.post["/appointment-request/reply"])],
},
async (req, res) => {
await appointmentReplySchema.validateAsync(req.body, { abortEarly: false })
@@ -261,12 +316,15 @@ export default (server: Server) => {
if (!appointment) throw Boom.notFound()
+ if (!appointment.applicant_id) {
+ throw Boom.internal("Applicant id not found.")
+ }
+
const [eligibleTrainingsForAppointment, user] = await Promise.all([
eligibleTrainingsForAppointmentService.getParameterByCleMinistereEducatif({
cleMinistereEducatif: appointment.cle_ministere_educatif,
}),
- // TODO applicant_id null | undefined | string
- users.getUserById(appointment.applicant_id as string),
+ users.getUserById(appointment.applicant_id),
])
if (!user || !eligibleTrainingsForAppointment) throw Boom.notFound()
diff --git a/server/src/http/routes/etablissement.controller.ts b/server/src/http/routes/etablissement.controller.ts
index 2711dad11d..e522f21152 100644
--- a/server/src/http/routes/etablissement.controller.ts
+++ b/server/src/http/routes/etablissement.controller.ts
@@ -39,6 +39,7 @@ export default (server: Server) => {
"/etablissements/:id",
{
schema: zRoutes.get["/etablissements/:id"],
+ onRequest: [server.auth(zRoutes.get["/etablissements/:id"])],
},
async (req, res) => {
const etablissement = await Etablissement.findById(req.params.id, etablissementProjection).lean()
@@ -58,6 +59,7 @@ export default (server: Server) => {
"/etablissements/:id/premium/affelnet/accept",
{
schema: zRoutes.post["/etablissements/:id/premium/affelnet/accept"],
+ onRequest: [server.auth(zRoutes.post["/etablissements/:id/premium/affelnet/accept"])],
},
async (req, res) => {
const etablissement = await Etablissement.findById(req.params.id)
@@ -179,9 +181,10 @@ export default (server: Server) => {
"/etablissements/:id/premium/accept",
{
schema: zRoutes.post["/etablissements/:id/premium/accept"],
+ onRequest: [server.auth(zRoutes.post["/etablissements/:id/premium/accept"])],
},
async (req, res) => {
- const etablissement = await Etablissement.findById(req.params.id)
+ const etablissement = await Etablissement.findById(req.params.id).lean()
if (!etablissement) {
throw Boom.badRequest("Etablissement not found.")
@@ -279,7 +282,7 @@ export default (server: Server) => {
)
const [result] = await Promise.all([
- Etablissement.findById(req.params.id),
+ Etablissement.findById(req.params.id).lean(),
...eligibleTrainingsForAppointmentsParcoursupFound.map((eligibleTrainingsForAppointment) =>
eligibleTrainingsForAppointmentService.update(
{ _id: eligibleTrainingsForAppointment._id, lieu_formation_email: { $nin: [null, ""] } },
@@ -303,6 +306,7 @@ export default (server: Server) => {
"/etablissements/:id/premium/affelnet/refuse",
{
schema: zRoutes.post["/etablissements/:id/premium/affelnet/refuse"],
+ onRequest: [server.auth(zRoutes.post["/etablissements/:id/premium/affelnet/refuse"])],
},
async (req, res) => {
const etablissement = await Etablissement.findById(req.params.id)
@@ -376,6 +380,7 @@ export default (server: Server) => {
"/etablissements/:id/premium/refuse",
{
schema: zRoutes.post["/etablissements/:id/premium/refuse"],
+ onRequest: [server.auth(zRoutes.post["/etablissements/:id/premium/refuse"])],
},
async (req, res) => {
const etablissement = await Etablissement.findById(req.params.id)
@@ -449,6 +454,7 @@ export default (server: Server) => {
"/etablissements/:id/appointments/:appointmentId",
{
schema: zRoutes.patch["/etablissements/:id/appointments/:appointmentId"],
+ onRequest: [server.auth(zRoutes.patch["/etablissements/:id/appointments/:appointmentId"])],
},
async ({ body, params }, res) => {
const { has_been_read } = body
diff --git a/server/src/jobs/rdv/inviteEtablissementToOptOut.ts b/server/src/jobs/rdv/inviteEtablissementToOptOut.ts
index bee22773e6..e6bb3dc600 100644
--- a/server/src/jobs/rdv/inviteEtablissementToOptOut.ts
+++ b/server/src/jobs/rdv/inviteEtablissementToOptOut.ts
@@ -1,6 +1,7 @@
import * as _ from "lodash-es"
import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
+import { createRdvaOptOutUnsubscribePageLink } from "@/services/appLinks.service"
import { logger } from "../../common/logger"
import { mailType } from "../../common/model/constants/etablissement"
@@ -68,7 +69,7 @@ export const inviteEtablissementToOptOut = async () => {
}
// Invite all etablissements only in production environment, for etablissement that have an "email_decisionnaire"
- if (emailDecisionaire) {
+ if (emailDecisionaire && etablissement.gestionnaire_email && etablissement.formateur_siret) {
const willBeActivatedAt = dayjs().add(15, "days")
const { messageId } = await mailer.sendEmail({
@@ -89,7 +90,7 @@ export const inviteEtablissementToOptOut = async () => {
formateur_city: etablissement.formateur_city,
siret: etablissement?.formateur_siret,
optOutActivatedAtDate: willBeActivatedAt.format("DD/MM"),
- linkToUnsubscribe: `${config.publicUrl}/espace-pro/form/opt-out/unsubscribe/${etablissement._id}`,
+ linkToUnsubscribe: createRdvaOptOutUnsubscribePageLink(etablissement.gestionnaire_email, etablissement.formateur_siret, etablissement._id.toString()),
},
user: {
destinataireEmail: emailDecisionaire,
diff --git a/server/src/jobs/rdv/inviteEtablissementToPremium.ts b/server/src/jobs/rdv/inviteEtablissementToPremium.ts
index c1e6a30647..02d130fcd3 100644
--- a/server/src/jobs/rdv/inviteEtablissementToPremium.ts
+++ b/server/src/jobs/rdv/inviteEtablissementToPremium.ts
@@ -1,5 +1,6 @@
import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
import { isValidEmail } from "@/common/utils/isValidEmail"
+import { createRdvaPremiumParcoursupPageLink } from "@/services/appLinks.service"
import { logger } from "../../common/logger"
import { mailType } from "../../common/model/constants/etablissement"
@@ -15,10 +16,10 @@ import mailer from "../../services/mailer.service"
export const inviteEtablissementToPremium = async () => {
logger.info("Cron #inviteEtablissementToPremium started.")
- const startInvitationPeriod = dayjs().month(0).date(1)
+ const startInvitationPeriod = dayjs().month(0).date(8)
const endInvitationPeriod = dayjs().month(7).date(31)
if (!dayjs().isBetween(startInvitationPeriod, endInvitationPeriod, "day", "[]")) {
- logger.info("Stopped because we are not between the 01/01 and the 31/08 (eligible period).")
+ logger.info("Stopped because we are not between the 08/01 and the 31/08 (eligible period).")
return
}
@@ -28,7 +29,9 @@ export const inviteEtablissementToPremium = async () => {
},
premium_activation_date: null,
"to_etablissement_emails.campaign": { $ne: mailType.PREMIUM_INVITE },
- })
+ }).lean()
+
+ logger.info("Cron #inviteEtablissementToPremium / Etablissement: ", etablissementsToInvite.length)
for (const etablissement of etablissementsToInvite) {
// Only send an invite if the "etablissement" have at least one available Parcoursup "formation"
@@ -38,7 +41,7 @@ export const inviteEtablissementToPremium = async () => {
parcoursup_id: { $ne: null },
}).lean()
- if (!hasOneAvailableFormation || !isValidEmail(etablissement.gestionnaire_email)) {
+ if (!hasOneAvailableFormation || !isValidEmail(etablissement.gestionnaire_email) || !etablissement.formateur_siret) {
continue
}
@@ -56,7 +59,7 @@ export const inviteEtablissementToPremium = async () => {
etablissement: {
email: etablissement.gestionnaire_email,
activatedAt: dayjs(etablissement.optout_activation_scheduled_date).format("DD/MM"),
- linkToForm: `${config.publicUrl}/espace-pro/form/premium/${etablissement._id}`,
+ linkToForm: createRdvaPremiumParcoursupPageLink(etablissement.gestionnaire_email, etablissement.formateur_siret, etablissement._id.toString()),
},
},
})
diff --git a/server/src/jobs/rdv/inviteEtablissementToPremiumAffelnet.ts b/server/src/jobs/rdv/inviteEtablissementToPremiumAffelnet.ts
index 6e16dcee06..287d01101a 100644
--- a/server/src/jobs/rdv/inviteEtablissementToPremiumAffelnet.ts
+++ b/server/src/jobs/rdv/inviteEtablissementToPremiumAffelnet.ts
@@ -1,4 +1,5 @@
import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
+import { createRdvaPremiumAffelnetPageLink } from "@/services/appLinks.service"
import { logger } from "../../common/logger"
import { mailType } from "../../common/model/constants/etablissement"
@@ -27,7 +28,7 @@ export const inviteEtablissementAffelnetToPremium = async () => {
})
for (const etablissement of etablissementToInvite) {
- if (!etablissement.gestionnaire_email) {
+ if (!etablissement.gestionnaire_email || !etablissement.formateur_siret) {
continue
}
@@ -45,7 +46,7 @@ export const inviteEtablissementAffelnetToPremium = async () => {
etablissement: {
email: etablissement.gestionnaire_email,
activatedAt: dayjs(etablissement.created_at).format("DD/MM"),
- linkToForm: `${config.publicUrl}/espace-pro/form/premium/affelnet/${etablissement._id}`,
+ linkToForm: createRdvaPremiumAffelnetPageLink(etablissement.gestionnaire_email, etablissement.formateur_siret, etablissement._id.toString()),
},
},
})
diff --git a/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUp.ts b/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUp.ts
index a39be6642d..45a228bb76 100644
--- a/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUp.ts
+++ b/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUp.ts
@@ -1,4 +1,5 @@
import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
+import { createRdvaPremiumParcoursupPageLink } from "@/services/appLinks.service"
import { logger } from "../../common/logger"
import { mailType } from "../../common/model/constants/etablissement"
@@ -35,7 +36,7 @@ export const inviteEtablissementToPremiumFollowUp = async () => {
})
for (const etablissement of etablissementsFound) {
- if (!etablissement.gestionnaire_email || !isValidEmail(etablissement.gestionnaire_email)) {
+ if (!etablissement.gestionnaire_email || !isValidEmail(etablissement.gestionnaire_email) || !etablissement.formateur_siret) {
continue
}
@@ -54,7 +55,7 @@ export const inviteEtablissementToPremiumFollowUp = async () => {
etablissement: {
email: etablissement.gestionnaire_email,
activatedAt: dayjs(etablissement.optout_activation_scheduled_date).format("DD/MM"),
- linkToForm: `${config.publicUrl}/espace-pro/form/premium/${etablissement._id}`,
+ linkToForm: createRdvaPremiumParcoursupPageLink(etablissement.gestionnaire_email, etablissement.formateur_siret, etablissement._id.toString()),
},
},
})
diff --git a/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUpAffelnet.ts b/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUpAffelnet.ts
index 829e0940af..06f293d9f2 100644
--- a/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUpAffelnet.ts
+++ b/server/src/jobs/rdv/inviteEtablissementToPremiumFollowUpAffelnet.ts
@@ -1,4 +1,5 @@
import { getStaticFilePath } from "@/common/utils/getStaticFilePath"
+import { createRdvaPremiumAffelnetPageLink } from "@/services/appLinks.service"
import { logger } from "../../common/logger"
import { mailType } from "../../common/model/constants/etablissement"
@@ -32,7 +33,7 @@ export const inviteEtablissementAffelnetToPremiumFollowUp = async () => {
})
for (const etablissement of etablissementsFound) {
- if (!etablissement.gestionnaire_email || !isValidEmail(etablissement.gestionnaire_email)) {
+ if (!etablissement.gestionnaire_email || !isValidEmail(etablissement.gestionnaire_email) || !etablissement.formateur_siret) {
continue
}
@@ -51,7 +52,7 @@ export const inviteEtablissementAffelnetToPremiumFollowUp = async () => {
etablissement: {
email: etablissement.gestionnaire_email,
activatedAt: dayjs(etablissement.created_at).format("DD/MM"),
- linkToForm: `${config.publicUrl}/espace-pro/form/premium/affelnet/${etablissement._id}`,
+ linkToForm: createRdvaPremiumAffelnetPageLink(etablissement.gestionnaire_email, etablissement.formateur_siret, etablissement._id.toString()),
},
},
})
diff --git a/server/src/services/appLinks.service.ts b/server/src/services/appLinks.service.ts
index 9ef44fdb91..f9f753f053 100644
--- a/server/src/services/appLinks.service.ts
+++ b/server/src/services/appLinks.service.ts
@@ -85,3 +85,169 @@ export function createCfaUnsubscribeToken(email: string, siret: string) {
}
)
}
+
+/**
+ * Forge a link for Affelnet premium activation.
+ */
+export function createRdvaPremiumAffelnetPageLink(email: string, siret: string, etablissementId: string): string {
+ const token = generateAccessToken(
+ { type: "cfa", email, siret },
+ [
+ generateScope({
+ schema: zRoutes.get["/etablissements/:id"],
+ options: {
+ params: { id: etablissementId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ },
+ }),
+ generateScope({
+ schema: zRoutes.post["/etablissements/:id/premium/affelnet/accept"],
+ options: {
+ params: { id: etablissementId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ },
+ }),
+ generateScope({
+ schema: zRoutes.post["/etablissements/:id/premium/affelnet/refuse"],
+ options: {
+ params: { id: etablissementId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ },
+ }),
+ ],
+ {
+ expiresIn: "30d",
+ }
+ )
+
+ return `${config.publicUrl}/espace-pro/form/premium/affelnet/${etablissementId}?token=${encodeURIComponent(token)}`
+}
+
+/**
+ * Forge a link for Parcoursup premium activation.
+ */
+export function createRdvaPremiumParcoursupPageLink(email: string, siret: string, etablissementId: string): string {
+ const token = generateAccessToken(
+ { type: "cfa", email, siret },
+ [
+ generateScope({
+ schema: zRoutes.get["/etablissements/:id"],
+ options: {
+ params: { id: etablissementId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ },
+ }),
+ generateScope({
+ schema: zRoutes.post["/etablissements/:id/premium/accept"],
+ options: {
+ params: { id: etablissementId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ },
+ }),
+ generateScope({
+ schema: zRoutes.post["/etablissements/:id/premium/refuse"],
+ options: {
+ params: { id: etablissementId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ },
+ }),
+ ],
+ {
+ expiresIn: "30d",
+ }
+ )
+
+ return `${config.publicUrl}/espace-pro/form/premium/${etablissementId}?token=${encodeURIComponent(token)}`
+}
+
+/**
+ * Forge a link for allwoing unsubscription.
+ */
+export function createRdvaOptOutUnsubscribePageLink(email: string, siret: string, etablissementId: string): string {
+ const token = generateAccessToken(
+ { type: "cfa", email, siret },
+ [
+ generateScope({
+ schema: zRoutes.get["/etablissements/:id/opt-out/unsubscribe"],
+ options: {
+ params: { id: etablissementId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ },
+ }),
+ ],
+ {
+ expiresIn: "30d",
+ }
+ )
+ return `${config.publicUrl}/espace-pro/form/opt-out/unsubscribe/${etablissementId}?token=${encodeURIComponent(token)}`
+}
+
+/**
+ * Forge a link for reading appointment
+ */
+export function createRdvaAppointmentIdPageLink(email: string, siret: string, etablissementId: string, appointmentId: string): string {
+ const token = generateAccessToken(
+ { type: "cfa", email, siret },
+ [
+ generateScope({
+ schema: zRoutes.patch["/etablissements/:id/appointments/:appointmentId"],
+ options: {
+ params: { id: etablissementId, appointmentId },
+ querystring: undefined,
+ },
+ resources: {
+ etablissement: [etablissementId],
+ appointment: [appointmentId],
+ },
+ }),
+ generateScope({
+ schema: zRoutes.get["/appointment-request/context/recap"],
+ options: {
+ params: undefined,
+ querystring: {
+ appointmentId,
+ },
+ },
+ resources: {
+ appointment: [appointmentId],
+ },
+ }),
+ generateScope({
+ schema: zRoutes.post["/appointment-request/reply"],
+ options: {
+ params: undefined,
+ querystring: undefined,
+ },
+ resources: {
+ appointment: [appointmentId],
+ },
+ }),
+ ],
+ {
+ expiresIn: "30d",
+ }
+ )
+
+ return `${config.publicUrl}/espace-pro/establishment/${etablissementId}/appointments/${appointmentId}?token=${encodeURIComponent(token)}`
+}
diff --git a/server/static/templates/mail-cfa-demande-de-contact.mjml.ejs b/server/static/templates/mail-cfa-demande-de-contact.mjml.ejs
index d9dc8e7740..dd1e07787f 100644
--- a/server/static/templates/mail-cfa-demande-de-contact.mjml.ejs
+++ b/server/static/templates/mail-cfa-demande-de-contact.mjml.ejs
@@ -24,7 +24,7 @@
<%= data.user.firstname %> <%= data.user.lastname %> souhaite des renseignements sur votre formation.
-
+
Voir les coordonnées du contact
diff --git a/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap b/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap
index a836603bc8..d898ccd037 100644
--- a/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap
+++ b/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap
@@ -2849,7 +2849,7 @@ exports[`generateOpenApiSchema > should generate proper schema 1`] = `
"required": true,
},
"responses": {
- "2xx": {
+ "200": {
"content": {
"application/json": {
"schema": {
diff --git a/shared/routes/appointments.routes.ts b/shared/routes/appointments.routes.ts
index a9f00318d4..7ac6dde8c1 100644
--- a/shared/routes/appointments.routes.ts
+++ b/shared/routes/appointments.routes.ts
@@ -119,7 +119,7 @@ export const zAppointmentsRoute = {
method: "get",
path: "/admin/appointments/details",
response: {
- "2xx": z
+ "200": z
.object({
appointments: z.array(
z
@@ -155,13 +155,38 @@ export const zAppointmentsRoute = {
ressources: {},
},
},
+ "/appointment-request/context/short-recap": {
+ method: "get",
+ path: "/appointment-request/context/short-recap",
+ querystring: z.object({ appointmentId: z.string() }).strict(),
+ response: {
+ "200": z
+ .object({
+ user: z
+ .object({
+ firstname: z.string(),
+ lastname: z.string(),
+ phone: z.string(),
+ email: z.string(),
+ })
+ .strict(),
+ etablissement: z
+ .object({
+ etablissement_formateur_raison_sociale: z.string().nullish(),
+ lieu_formation_email: z.string().nullish(),
+ })
+ .strict(),
+ })
+ .strict(),
+ },
+ securityScheme: null,
+ },
"/appointment-request/context/recap": {
method: "get",
path: "/appointment-request/context/recap",
- // TODO_SECURITY_FIX il faut un secure token
querystring: z.object({ appointmentId: z.string() }).strict(),
response: {
- "2xx": z
+ "200": z
.object({
appointment: z
.object({
@@ -199,7 +224,11 @@ export const zAppointmentsRoute = {
})
.strict(),
},
- securityScheme: null,
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
+ },
},
},
post: {
@@ -208,7 +237,7 @@ export const zAppointmentsRoute = {
path: "/appointment-request/context/create",
body: zContextCreateSchema,
response: {
- "2xx": zAppointmentRequestContextCreateResponseSchema,
+ "200": zAppointmentRequestContextCreateResponseSchema,
"404": z.union([ZResError, z.literal("Formation introuvable")]),
"400": z.union([ZResError, z.literal("Critère de recherche non conforme.")]),
},
@@ -237,8 +266,8 @@ export const zAppointmentsRoute = {
.strict(),
response: {
// TODO ANY TO BE FIXED
- "2xx": z.any(),
- // "2xx": z
+ "200": z.any(),
+ // "200": z
// .object({
// userId: z.string(),
// appointment: z.union([ZAppointment, z.null()]),
@@ -250,7 +279,6 @@ export const zAppointmentsRoute = {
"/appointment-request/reply": {
method: "post",
path: "/appointment-request/reply",
- // TODO_SECURITY_FIX token jwt
body: z
.object({
appointment_id: z.string(),
@@ -260,7 +288,7 @@ export const zAppointmentsRoute = {
})
.strict(),
response: {
- "2xx": z
+ "200": z
.object({
appointment_id: z.string(),
cfa_intention_to_applicant: z.string(),
@@ -269,7 +297,11 @@ export const zAppointmentsRoute = {
})
.strict(),
},
- securityScheme: null,
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
+ },
},
},
} as const satisfies IRoutesDef
diff --git a/shared/routes/etablissement.routes.ts b/shared/routes/etablissement.routes.ts
index d423d5f0eb..41343c7bb6 100644
--- a/shared/routes/etablissement.routes.ts
+++ b/shared/routes/etablissement.routes.ts
@@ -1,6 +1,6 @@
import { extensions } from "../helpers/zodHelpers/zodPrimitives"
import { z } from "../helpers/zodWithOpenApi"
-import { ZEtablissement } from "../models"
+import { ZAppointment, ZEtablissement } from "../models"
import { zObjectId } from "../models/common"
import { IRoutesDef } from "./common.routes"
@@ -12,7 +12,7 @@ export const zEtablissementRoutes = {
path: "/admin/etablissements/siret-formateur/:siret",
params: z.object({ siret: extensions.siret }).strict(),
response: {
- "2xx": ZEtablissement.strict(),
+ "200": ZEtablissement.strict(),
},
securityScheme: {
auth: "cookie-session",
@@ -31,7 +31,7 @@ export const zEtablissementRoutes = {
path: "/admin/etablissements/:id",
params: z.object({ id: zObjectId }).strict(),
response: {
- "2xx": ZEtablissement.strict(),
+ "200": ZEtablissement.strict(),
},
securityScheme: {
auth: "cookie-session",
@@ -50,7 +50,7 @@ export const zEtablissementRoutes = {
path: "/etablissements/:id",
params: z.object({ id: zObjectId }).strict(),
response: {
- "2xx": ZEtablissement.pick({
+ "200": ZEtablissement.pick({
_id: true,
optout_refusal_date: true,
raison_sociale: true,
@@ -64,7 +64,11 @@ export const zEtablissementRoutes = {
premium_refusal_date: true,
}).strict(),
},
- securityScheme: null,
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
+ },
},
},
post: {
@@ -73,36 +77,52 @@ export const zEtablissementRoutes = {
path: "/etablissements/:id/premium/affelnet/accept",
params: z.object({ id: zObjectId }).strict(),
response: {
- "2xx": ZEtablissement,
+ "200": ZEtablissement,
+ },
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
},
- securityScheme: null,
},
- "/etablissements/:id/premium/accept": {
+ "/etablissements/:id/premium/affelnet/refuse": {
method: "post",
- path: "/etablissements/:id/premium/accept",
+ path: "/etablissements/:id/premium/affelnet/refuse",
params: z.object({ id: zObjectId }).strict(),
response: {
- "2xx": ZEtablissement,
+ "200": ZEtablissement,
+ },
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
},
- securityScheme: null,
},
- "/etablissements/:id/premium/affelnet/refuse": {
+ "/etablissements/:id/premium/accept": {
method: "post",
- path: "/etablissements/:id/premium/affelnet/refuse",
+ path: "/etablissements/:id/premium/accept",
params: z.object({ id: zObjectId }).strict(),
response: {
- "2xx": ZEtablissement,
+ "200": ZEtablissement,
+ },
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
},
- securityScheme: null,
},
"/etablissements/:id/premium/refuse": {
method: "post",
path: "/etablissements/:id/premium/refuse",
params: z.object({ id: zObjectId }).strict(),
response: {
- "2xx": ZEtablissement,
+ "200": ZEtablissement,
+ },
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
},
- securityScheme: null,
},
"/etablissements/:id/opt-out/unsubscribe": {
method: "post",
@@ -111,7 +131,7 @@ export const zEtablissementRoutes = {
params: z.object({ id: zObjectId }).strict(),
body: z.union([z.object({ opt_out_question: z.string() }).strict(), z.object({}).strict()]),
response: {
- "2xx": ZEtablissement,
+ "200": ZEtablissement,
},
securityScheme: null,
},
@@ -125,7 +145,7 @@ export const zEtablissementRoutes = {
gestionnaire_email: true,
}).strict(),
response: {
- "2xx": ZEtablissement,
+ "200": ZEtablissement,
},
securityScheme: {
auth: "cookie-session",
@@ -142,15 +162,16 @@ export const zEtablissementRoutes = {
"/etablissements/:id/appointments/:appointmentId": {
method: "patch",
path: "/etablissements/:id/appointments/:appointmentId",
- // TODO_SECURITY_FIX ajouter un jwt
body: z.object({ has_been_read: z.boolean() }).strict(),
params: z.object({ id: zObjectId, appointmentId: zObjectId }).strict(),
response: {
- // TODO ANY TO BE FIXED
- "2xx": z.any(),
- // "2xx": ZAppointment,
+ "200": ZAppointment,
+ },
+ securityScheme: {
+ auth: "access-token",
+ access: null,
+ ressources: {},
},
- securityScheme: null,
},
},
} as const satisfies IRoutesDef
diff --git a/shared/security/permissions.ts b/shared/security/permissions.ts
index a4ae1d9f7b..d5e5e7fcbb 100644
--- a/shared/security/permissions.ts
+++ b/shared/security/permissions.ts
@@ -61,6 +61,9 @@ export type AccessRessouces = {
etablissement?: ReadonlyArray<{
_id: AccessResourcePath
}>
+ appointment?: ReadonlyArray<{
+ _id: AccessResourcePath
+ }>
formationCatalogue?: ReadonlyArray<{
cle_ministere_educatif: AccessResourcePath
}>
diff --git a/ui/components/RDV/DemandeDeContact.tsx b/ui/components/RDV/DemandeDeContact.tsx
index 9c601c6763..365d545a49 100644
--- a/ui/components/RDV/DemandeDeContact.tsx
+++ b/ui/components/RDV/DemandeDeContact.tsx
@@ -28,12 +28,12 @@ import {
} from "@chakra-ui/react"
import emailMisspelled, { top100 } from "email-misspelled"
import { useFormik } from "formik"
-import React, { useState } from "react"
+import { useState } from "react"
import { EApplicantType, EReasonsKey } from "shared"
import { IAppointmentRequestContextCreateFormAvailableResponseSchema } from "shared/routes/appointments.routes"
import * as Yup from "yup"
-import { IAppointmentRequestRecapResponse, reasons } from "@/components/RDV/types"
+import { reasons } from "@/components/RDV/types"
import { BarberGuy } from "@/theme/components/icons"
import { PaperPlane } from "@/theme/components/icons/PaperPlane"
import { apiGet, apiPost } from "@/utils/api.utils"
@@ -52,7 +52,7 @@ const DemandeDeContact = (props: Props) => {
const [suggestedEmails, setSuggestedEmails] = useState([])
const [applicantReasons, setApplicantReasons] = useState(reasons)
const [applicantType, setApplicantType] = useState(EApplicantType.ETUDIANT)
- const [onSuccessSubmitResponse, setOnSuccessSubmitResponse] = useState(null)
+ const [onSuccessSubmitResponse, setOnSuccessSubmitResponse] = useState(null)
const [error, setError] = useState(null)
const emailChecker = emailMisspelled({ maxMisspelled: 3, domains: top100 })
@@ -113,7 +113,7 @@ const DemandeDeContact = (props: Props) => {
},
})) as unknown as { appointment: { _id: string } }
- const response = await apiGet("/appointment-request/context/recap", {
+ const response = await apiGet("/appointment-request/context/short-recap", {
querystring: { appointmentId: appointment._id },
})
diff --git a/ui/components/RDV/types.ts b/ui/components/RDV/types.ts
index 9055e7ce58..8599a0c9f2 100644
--- a/ui/components/RDV/types.ts
+++ b/ui/components/RDV/types.ts
@@ -30,6 +30,19 @@ type IAppointmentRequestRecapResponse = {
}
}
+type IAppointmentRequestRecapResponseShort = {
+ user: {
+ firstname: string
+ lastname: string
+ phone: string
+ email: string
+ }
+ etablissement: {
+ etablissement_formateur_raison_sociale: string
+ lieu_formation_email: string
+ }
+}
+
const reasons = [
{
key: EReasonsKey.MODALITE,
@@ -88,6 +101,6 @@ const reasons = [
},
]
-export type { IAppointmentRequestRecapResponse }
+export type { IAppointmentRequestRecapResponse, IAppointmentRequestRecapResponseShort }
export { reasons }
diff --git a/ui/pages/espace-pro/admin/eligible-trainings-for-appointment/edit/[id].tsx b/ui/pages/espace-pro/admin/eligible-trainings-for-appointment/edit/[id].tsx
index 0c9a5d70c8..f93cd84809 100644
--- a/ui/pages/espace-pro/admin/eligible-trainings-for-appointment/edit/[id].tsx
+++ b/ui/pages/espace-pro/admin/eligible-trainings-for-appointment/edit/[id].tsx
@@ -24,8 +24,8 @@ import {
import emailValidator from "email-validator"
import Head from "next/head"
import { useRouter } from "next/router"
-import React, { createRef, useEffect, useState } from "react"
-import { IEtablissement } from "shared"
+import { createRef, useEffect, useState } from "react"
+import { IEtablissementJson } from "shared"
import { referrers } from "shared/constants/referers"
import { IEligibleTrainingsForAppointmentJson } from "shared/models/elligibleTraining.model"
@@ -46,7 +46,7 @@ function EditPage() {
const router = useRouter()
const { id } = router.query
const [eligibleTrainingsForAppointmentResult, setEligibleTrainingsForAppointmentResult] = useState([])
- const [etablissement, setEtablissement] = useState()
+ const [etablissement, setEtablissement] = useState()
const [loading, setLoading] = useState(false)
const toast = useToast()
diff --git a/ui/pages/espace-pro/establishment/[establishmentId]/appointments/[appointmentId].tsx b/ui/pages/espace-pro/establishment/[establishmentId]/appointments/[appointmentId].tsx
index af62dacdfc..20f5a953b8 100644
--- a/ui/pages/espace-pro/establishment/[establishmentId]/appointments/[appointmentId].tsx
+++ b/ui/pages/espace-pro/establishment/[establishmentId]/appointments/[appointmentId].tsx
@@ -20,7 +20,7 @@ import { CfaCandidatInformationUnreachable } from "../../../../../components/esp
*/
export default function CfaCandidatInformationPage() {
const router = useRouter()
- const { establishmentId, appointmentId } = router.query as { establishmentId: string; appointmentId: string }
+ const { establishmentId, appointmentId, token } = router.query as { establishmentId: string; appointmentId: string; token: string }
const [data, setData] = useState(null)
const [currentState, setCurrentState] = useState("initial")
@@ -42,6 +42,9 @@ export default function CfaCandidatInformationPage() {
cfa_message_to_applicant: values.message,
cfa_message_to_applicant_date: formatDate(new Date()),
},
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setCurrentState("answered")
@@ -58,6 +61,9 @@ export default function CfaCandidatInformationPage() {
cfa_message_to_applicant: "",
cfa_message_to_applicant_date: formatDate(new Date()),
},
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setCurrentState("other")
}
@@ -71,6 +77,9 @@ export default function CfaCandidatInformationPage() {
cfa_message_to_applicant: "",
cfa_message_to_applicant_date: formatDate(new Date()),
},
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setCurrentState("unreachable")
}
@@ -83,11 +92,20 @@ export default function CfaCandidatInformationPage() {
const fetchData = async () => {
if (appointmentId && establishmentId) {
if (utmSource === "mail") {
- await apiPatch("/etablissements/:id/appointments/:appointmentId", { params: { id: establishmentId, appointmentId }, body: { has_been_read: true } })
+ await apiPatch("/etablissements/:id/appointments/:appointmentId", {
+ params: { id: establishmentId, appointmentId },
+ body: { has_been_read: true },
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ })
}
const response = await apiGet("/appointment-request/context/recap", {
querystring: { appointmentId },
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setData(response)
}
diff --git a/ui/pages/espace-pro/form/index.tsx b/ui/pages/espace-pro/form/index.tsx
index d4f17234af..a8efc50f5b 100644
--- a/ui/pages/espace-pro/form/index.tsx
+++ b/ui/pages/espace-pro/form/index.tsx
@@ -1,7 +1,7 @@
import { Box, Spinner, Text } from "@chakra-ui/react"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
-import { IAppointmentRequestContextCreateResponseSchema, IAppointmentRequestContextCreateFormAvailableResponseSchema } from "shared/routes/appointments.routes"
+import { IAppointmentRequestContextCreateFormAvailableResponseSchema, IAppointmentRequestContextCreateResponseSchema } from "shared/routes/appointments.routes"
import { ContactCfaSummary } from "@/components/espace_pro/Candidat/layout/ContactCfaSummary"
import DemandeDeContact from "@/components/RDV/DemandeDeContact"
diff --git a/ui/pages/espace-pro/form/opt-out/unsubscribe/[id].tsx b/ui/pages/espace-pro/form/opt-out/unsubscribe/[id].tsx
index 3cc11bab1f..831f7fa2a5 100644
--- a/ui/pages/espace-pro/form/opt-out/unsubscribe/[id].tsx
+++ b/ui/pages/espace-pro/form/opt-out/unsubscribe/[id].tsx
@@ -1,8 +1,8 @@
import { Box, Button, Container, Flex, Heading, Radio, RadioGroup, Stack, Text, Textarea } from "@chakra-ui/react"
import Head from "next/head"
import { useRouter } from "next/router"
-import React, { useEffect, useState } from "react"
-import { IEtablissement } from "shared"
+import { useEffect, useState } from "react"
+import { IEtablissementJson } from "shared"
import { apiGet, apiPost } from "@/utils/api.utils"
@@ -11,7 +11,7 @@ import Layout from "../../../../../components/espace_pro/common/components/Layou
import { SuccessCircle } from "../../../../../theme/components/icons"
type IEtablissementPartial = Pick<
- IEtablissement,
+ IEtablissementJson,
| "_id"
| "optout_refusal_date"
| "raison_sociale"
@@ -36,7 +36,7 @@ export default function OptOutUnsubscribe() {
}
const router = useRouter()
- const { id } = router.query as { id: string }
+ const { id, token } = router.query as { id: string; token: string }
const [textarea, setTextarea] = useState("")
const [hasBeenUnsubscribed, setHasBeenUnsubscribed] = useState(false)
const [isQuestionSent, setIsQuestionSent] = useState(false)
@@ -71,9 +71,12 @@ export default function OptOutUnsubscribe() {
useEffect(() => {
const fetchData = async () => {
- const etablissement = (await apiGet("/etablissements/:id", {
+ const etablissement = await apiGet("/etablissements/:id", {
params: { id },
- })) as any // TODO not any
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ })
if (etablissement.optout_refusal_date) {
setHasBeenUnsubscribed(true)
diff --git a/ui/pages/espace-pro/form/premium/[id].tsx b/ui/pages/espace-pro/form/premium/[id].tsx
index fe9e2decf2..54115f7fef 100644
--- a/ui/pages/espace-pro/form/premium/[id].tsx
+++ b/ui/pages/espace-pro/form/premium/[id].tsx
@@ -1,7 +1,8 @@
import { Box, Button, Container, Flex, Stack, Text } from "@chakra-ui/react"
import Head from "next/head"
import { useRouter } from "next/router"
-import React, { useEffect, useState } from "react"
+import { useEffect, useState } from "react"
+import { IEtablissementJson } from "shared"
import { apiGet, apiPost } from "@/utils/api.utils"
@@ -25,7 +26,7 @@ type IPremiumEtablissement = {
*/
export default function PremiumForm() {
const router = useRouter()
- const { id } = router.query as { id: string }
+ const { id, token } = router.query as { id: string; token: string }
const [hasRefused, setHasRefused] = useState(false)
const [hasAccepted, setHasAccepted] = useState(false)
const [etablissement, setEtablissement]: [IPremiumEtablissement | null, (e: any) => void] = useState()
@@ -39,6 +40,9 @@ export default function PremiumForm() {
const accept = async () => {
await apiPost("/etablissements/:id/premium/accept", {
params: { id },
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setHasAccepted(true)
window.scrollTo(0, 0)
@@ -51,6 +55,9 @@ export default function PremiumForm() {
const refuse = async () => {
await apiPost("/etablissements/:id/premium/refuse", {
params: { id },
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setHasRefused(true)
window.scrollTo(0, 0)
@@ -60,7 +67,10 @@ export default function PremiumForm() {
const fetchData = async () => {
const etablissement = (await apiGet("/etablissements/:id", {
params: { id },
- })) as any // TODO not any
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ })) as IEtablissementJson
if (etablissement.premium_refusal_date) {
setHasRefused(true)
diff --git a/ui/pages/espace-pro/form/premium/affelnet/[id].tsx b/ui/pages/espace-pro/form/premium/affelnet/[id].tsx
index fd7f724b0c..3a2a9a95a7 100644
--- a/ui/pages/espace-pro/form/premium/affelnet/[id].tsx
+++ b/ui/pages/espace-pro/form/premium/affelnet/[id].tsx
@@ -1,7 +1,8 @@
import { Box, Button, Container, Flex, Stack, Text } from "@chakra-ui/react"
import Head from "next/head"
import { useRouter } from "next/router"
-import React, { useEffect, useState } from "react"
+import { useEffect, useState } from "react"
+import { IEtablissementJson } from "shared"
import { apiGet, apiPost } from "@/utils/api.utils"
@@ -25,7 +26,7 @@ type IAffelnetEtablissement = {
*/
export default function PremiumAffelnetForm() {
const router = useRouter()
- const { id } = router.query as { id: string }
+ const { id, token } = router.query as { id: string; token: string }
const [hasRefused, setHasRefused] = useState(false)
const [hasAccepted, setHasAccepted] = useState(false)
const [etablissement, setEtablissement]: [IAffelnetEtablissement | null, (e: any) => void] = useState()
@@ -39,6 +40,9 @@ export default function PremiumAffelnetForm() {
const accept = async () => {
await apiPost("/etablissements/:id/premium/affelnet/accept", {
params: { id },
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setHasAccepted(true)
@@ -52,6 +56,9 @@ export default function PremiumAffelnetForm() {
const refuse = async () => {
await apiPost("/etablissements/:id/premium/affelnet/refuse", {
params: { id },
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
})
setHasRefused(true)
window.scrollTo(0, 0)
@@ -61,7 +68,10 @@ export default function PremiumAffelnetForm() {
const fetchData = async () => {
const etablissement = (await apiGet("/etablissements/:id", {
params: { id },
- })) as any // TODO not any
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ })) as IEtablissementJson
if (etablissement.premium_affelnet_refusal_date) {
setHasRefused(true)
diff --git a/ui/services/fetchPrdv.ts b/ui/services/fetchPrdv.ts
index 69babbec24..4e2f1383b9 100644
--- a/ui/services/fetchPrdv.ts
+++ b/ui/services/fetchPrdv.ts
@@ -30,6 +30,7 @@ export default async function fetchPrdv(training, hasAlsoJob, _axios = axios, _w
},
{ headers: { "Content-Type": "application/json" } }
)
+
return response.data
} catch (error) {
if (error?.response?.data?.error === "Prise de rendez-vous non disponible." || error?.response?.data?.message === "Formation introuvable") {
diff --git a/ui/utils/api.utils.ts b/ui/utils/api.utils.ts
index 872232bc5f..fb9e6f28cc 100644
--- a/ui/utils/api.utils.ts
+++ b/ui/utils/api.utils.ts
@@ -1,5 +1,5 @@
import { IDeleteRoutes, IGetRoutes, IPatchRoutes, IPostRoutes, IPutRoutes, IRequest, IRequestFetchOptions, IResponse } from "shared"
-import { generateUri, WithQueryStringAndPathParam, PathParam, QueryString } from "shared/helpers/generateUri"
+import { PathParam, QueryString, WithQueryStringAndPathParam, generateUri } from "shared/helpers/generateUri"
import { IResErrorJson, IRouteSchema, IRouteSchemaWrite } from "shared/routes/common.routes"
import type { EmptyObject } from "type-fest"
import z, { ZodType } from "zod"