Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ajout de monitoring sur la création de CFA #878

Merged
merged 11 commits into from
Dec 7, 2023
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { IUserRecruteur, IUserStatusValidation } from "shared"
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
import { ETAT_UTILISATEUR, VALIDATION_UTILISATEUR } from "shared/constants/recruteur"

import { VALIDATION_UTILISATEUR } from "../../../../services/constant.service"
import { model, Schema } from "../../../mongodb"
import { mongoosePagination, Pagination } from "../_shared/mongoose-paginate"

Expand Down
17 changes: 8 additions & 9 deletions server/src/http/routes/etablissementRecruteur.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ETAT_UTILISATEUR, RECRUITER_STATUS } from "shared/constants/recruteur"

import { Recruiter, UserRecruteur } from "@/common/model"
import { startSession } from "@/common/utils/session.service"
import config from "@/config"
import { getUserFromRequest } from "@/security/authenticationService"

import { getAllDomainsFromEmailList, getEmailDomain, isEmailFromPrivateCompany, isUserMailExistInReferentiel } from "../../common/utils/mailUtils"
Expand Down Expand Up @@ -177,15 +178,16 @@ export default (server: Server) => {
const { contacts } = siretInfos

// Creation de l'utilisateur en base de données
let newCfa: IUserRecruteur = await createUser({ ...req.body, ...siretInfos })
let newCfa: IUserRecruteur = await createUser({ ...req.body, ...siretInfos, is_email_checked: false })

const slackNotification = {
subject: "RECRUTEUR",
message: `Nouvel OF en attente de validation - ${config.publicUrl}/espace-pro/administration/users/${newCfa._id}`,
}
if (!contacts.length) {
// Validation manuelle de l'utilisateur à effectuer pas un administrateur
newCfa = await setUserHasToBeManuallyValidated(newCfa._id)
await notifyToSlack({
subject: "RECRUTEUR",
message: `Nouvel OF en attente de validation - https://referentiel.apprentissage.beta.gouv.fr/organismes/${newCfa.establishment_siret}`,
})
await notifyToSlack(slackNotification)
return res.status(200).send({ user: newCfa })
}
if (isUserMailExistInReferentiel(contacts, email)) {
Expand All @@ -208,10 +210,7 @@ export default (server: Server) => {
}
// Validation manuelle de l'utilisateur à effectuer pas un administrateur
newCfa = await setUserHasToBeManuallyValidated(newCfa._id)
await notifyToSlack({
subject: "RECRUTEUR",
message: `Nouvel OF en attente de validation - https://referentiel.apprentissage.beta.gouv.fr/organismes/${newCfa.establishment_siret}`,
})
await notifyToSlack(slackNotification)
// Keep the same structure as ENTREPRISE
return res.status(200).send({ user: newCfa })
}
Expand Down
15 changes: 13 additions & 2 deletions server/src/http/routes/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Boom from "boom"
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
import { ETAT_UTILISATEUR, VALIDATION_UTILISATEUR } from "shared/constants/recruteur"
import { IJob, getUserStatus, zRoutes } from "shared/index"

import { stopSession } from "@/common/utils/session.service"
import { getUserFromRequest } from "@/security/authenticationService"

import { Recruiter, UserRecruteur } from "../../common/model/index"
import { getStaticFilePath } from "../../common/utils/getStaticFilePath"
Expand Down Expand Up @@ -97,7 +98,17 @@ export default (server: Server) => {
onRequest: [server.auth(zRoutes.post["/admin/users"])],
},
async (req, res) => {
const user = await createUser(req.body)
const user = await createUser({
...req.body,
is_email_checked: true,
status: [
{
status: ETAT_UTILISATEUR.ATTENTE,
validation_type: VALIDATION_UTILISATEUR.MANUAL,
user: getUserFromRequest(req, zRoutes.post["/admin/users"]).value._id.toString(),
},
],
})
return res.status(200).send(user)
}
)
Expand Down
10 changes: 0 additions & 10 deletions server/src/jobs/jobs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"

import { createMongoDBIndexes } from "@/common/model"
import { IInternalJobsCronTask, IInternalJobsSimple } from "@/common/model/schema/internalJobs/internalJobs.types"
import { create as createMigration, status as statusMigration, up as upMigration } from "@/jobs/migrations/migrations"
Expand Down Expand Up @@ -235,14 +233,6 @@ export async function runJob(job: IInternalJobsCronTask | IInternalJobsSimple):
address,
email,
scope,
status: [
{
status: ETAT_UTILISATEUR.VALIDE,
validation_type: "AUTOMATIQUE",
user: "SERVEUR",
date: new Date(),
},
],
},
{
options: {
Expand Down
19 changes: 12 additions & 7 deletions server/src/jobs/lba_recruteur/formulaire/createUser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
import { IUserRecruteur } from "shared/models"

import { logger } from "../../../common/logger"
Expand All @@ -13,8 +14,7 @@ export const createUserFromCLI = async (
address,
email,
scope,
status,
}: Pick<IUserRecruteur, "first_name" | "last_name" | "establishment_siret" | "establishment_raison_sociale" | "phone" | "address" | "email" | "scope" | "status">,
}: Pick<IUserRecruteur, "first_name" | "last_name" | "establishment_siret" | "establishment_raison_sociale" | "phone" | "address" | "email" | "scope">,
{ options }: { options: { Type: IUserRecruteur["type"]; Email_valide: IUserRecruteur["is_email_checked"] } }
) => {
const { Type, Email_valide } = options
Expand All @@ -25,7 +25,7 @@ export const createUserFromCLI = async (
return
}

const payload = {
await createUser({
first_name,
last_name,
establishment_siret,
Expand All @@ -36,10 +36,15 @@ export const createUserFromCLI = async (
scope,
type: Type,
is_email_checked: Email_valide,
status,
}

await createUser(payload)
status: [
{
status: ETAT_UTILISATEUR.VALIDE,
validation_type: "AUTOMATIQUE",
user: "SERVEUR",
date: new Date(),
},
],
})

logger.info(`User created : ${email} — ${scope} - admin: ${Type === "ADMIN"}`)
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
import { ETAT_UTILISATEUR, RECRUITER_STATUS, VALIDATION_UTILISATEUR } from "shared/constants/recruteur"

import { logger } from "../../../../common/logger"
import { Recruiter, UserRecruteur } from "../../../../common/model/index"
import { asyncForEach } from "../../../../common/utils/asyncUtils"
import { RECRUITER_STATUS, VALIDATION_UTILISATEUR } from "../../../../services/constant.service"
import { runScript } from "../../../scriptWrapper"

function hasUpperCase(str) {
Expand Down
4 changes: 0 additions & 4 deletions server/src/services/constant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ export const KEY_GENERATOR_PARAMS = ({ length, symbols, numbers }) => {
exclude: '!"_%£$€*¨^=+~ß(){}[]§;,./:`@#&|<>?"',
}
}
export enum VALIDATION_UTILISATEUR {
AUTO = "AUTOMATIQUE",
MANUAL = "MANUELLE",
}
export const ENTREPRISE_DELEGATION = "ENTREPRISE_DELEGATION"

export const ADMIN = "ADMIN"
Expand Down
36 changes: 22 additions & 14 deletions server/src/services/etablissement.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AxiosResponse } from "axios"
import Boom from "boom"
import type { FilterQuery } from "mongoose"
import { IEtablissement, ILbaCompany, IRecruiter, IReferentielData, IReferentielOpco, IUserRecruteur } from "shared"
import { IEtablissement, ILbaCompany, IRecruiter, IReferentielData, IReferentielOpco, IUserRecruteur, ZUserRecruteurReferentielData } from "shared"
import { EDiffusibleStatus } from "shared/constants/diffusibleStatus"
import { BusinessErrorCodes } from "shared/constants/errorCodes"
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
Expand Down Expand Up @@ -466,18 +466,26 @@ export const formatEntrepriseData = (d: IEtablissementGouv): IFormatAPIEntrepris
* @param {IReferentiel} d
* @returns {Object}
*/
export const formatReferentielData = (d: IReferentiel): IReferentielData => ({
establishment_state: d.etat_administratif,
is_qualiopi: d.qualiopi,
establishment_siret: d.siret,
establishment_raison_sociale: d.raison_sociale,
contacts: d.contacts,
address_detail: d.adresse,
address: d.adresse?.label,
geo_coordinates: d.adresse
? `${d.adresse?.geojson.geometry.coordinates[1]},${d.adresse?.geojson.geometry.coordinates[0]}`
: `${d.lieux_de_formation[0]?.adresse?.geojson?.geometry.coordinates[0]},${d.lieux_de_formation[0]?.adresse?.geojson?.geometry.coordinates[1]}`,
})
export const formatReferentielData = (d: IReferentiel): IReferentielData => {
const referentielData = {
establishment_state: d.etat_administratif,
is_qualiopi: d.qualiopi,
establishment_siret: d.siret,
establishment_raison_sociale: d.raison_sociale,
contacts: d.contacts,
address_detail: d.adresse,
address: d.adresse?.label,
geo_coordinates: d.adresse
? `${d.adresse?.geojson.geometry.coordinates[1]},${d.adresse?.geojson.geometry.coordinates[0]}`
: `${d.lieux_de_formation[0]?.adresse?.geojson?.geometry.coordinates[0]},${d.lieux_de_formation[0]?.adresse?.geojson?.geometry.coordinates[1]}`,
}

const validation = ZUserRecruteurReferentielData.safeParse(referentielData)
if (!validation.success) {
sentryCaptureException(Boom.internal(`address format error for siret=${d.siret}.`, { validationError: validation.error }))
}
return referentielData
}

/**
* Taggue l'organisme de formation pour qu'il ne reçoive plus de demande de délégation
Expand Down Expand Up @@ -721,7 +729,7 @@ export const entrepriseOnboardingWorkflow = {
cfa_delegated_siret,
})
const formulaireId = formulaireInfo.establishment_id
let newEntreprise: IUserRecruteur = await createUser({ ...savedData, establishment_id: formulaireId, type: ENTREPRISE })
let newEntreprise: IUserRecruteur = await createUser({ ...savedData, establishment_id: formulaireId, type: ENTREPRISE, is_email_checked: false })

if (hasSiretError) {
newEntreprise = await setUserInError(newEntreprise._id, "Erreur lors de l'appel à l'API SIRET")
Expand Down
34 changes: 16 additions & 18 deletions server/src/services/userRecruteur.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { randomUUID } from "crypto"
import Boom from "boom"
import type { FilterQuery, ModelUpdateOptions, UpdateQuery } from "mongoose"
import { IUserRecruteur, IUserRecruteurWritable, IUserStatusValidation } from "shared"
import { ETAT_UTILISATEUR } from "shared/constants/recruteur"
import { CFA, ENTREPRISE, ETAT_UTILISATEUR, VALIDATION_UTILISATEUR } from "shared/constants/recruteur"

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

import { UserRecruteur } from "../common/model/index"
import config from "../config"

import { createAuthMagicLink } from "./appLinks.service"
import { ADMIN, CFA, ENTREPRISE, VALIDATION_UTILISATEUR } from "./constant.service"
import { ADMIN } from "./constant.service"
import mailer from "./mailer.service"

/**
Expand Down Expand Up @@ -50,39 +50,37 @@ export const getUsers = async (query: FilterQuery<IUserRecruteur>, options, { pa
export const getUser = async (query: FilterQuery<IUserRecruteur>): Promise<IUserRecruteur | null> => UserRecruteur.findOne(query).lean()

/**
* @description create user
* @param {IUserRecruteur} values
* @returns {IUserRecruteur}
* @description création d'un nouveau user recruteur. Le champ status peut être passé ou, s'il n'est pas passé, être sauvé ultérieurement
*/
export const createUser = async (values) => {
let scope = values.scope ?? undefined
export const createUser = async (
userRecruteurProps: Omit<IUserRecruteur, "_id" | "createdAt" | "updatedAt" | "status"> & Partial<Pick<IUserRecruteur, "status">>
): Promise<IUserRecruteur> => {
let scope = userRecruteurProps.scope ?? undefined

const formatedEmail = values.email.toLocaleLowerCase()
const formatedEmail = userRecruteurProps.email.toLocaleLowerCase()

if (!scope) {
if (values.type === "CFA") {
if (userRecruteurProps.type === "CFA") {
// generate user scope
const [key] = randomUUID().split("-")
scope = `cfa-${key}`
} else {
let key
if (values?.establishment_raison_sociale) {
key = values.establishment_raison_sociale.toLowerCase().replace(/ /g, "-")
if (userRecruteurProps?.establishment_raison_sociale) {
key = userRecruteurProps.establishment_raison_sociale.toLowerCase().replace(/ /g, "-")
} else {
key = randomUUID().split("-")[0]
}
scope = `etp-${key}`
}
}

const user = new UserRecruteur({
...values,
scope: scope,
const createdUser = await UserRecruteur.create({
status: [],
...userRecruteurProps,
scope,
email: formatedEmail,
})

await user.save()
return user.toObject()
return createdUser.toObject()
}

/**
Expand Down
4 changes: 0 additions & 4 deletions server/tests/integration/http/rolesRoutes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,24 @@ describe("rolesRoutes", () => {
const httpClient = useServer()
it.skip("Vérifie qu'on peut se connecter à une route sécurisée en tant qu'administrateur", async () => {
const bearerToken = await createAndLogUser(httpClient, "userAdmin", { type: "ADMIN" })

const response = await httpClient().inject({ method: "GET", path: "/api/authentified", headers: bearerToken })
expect(response.statusCode).toBe(200)
})

it("Vérifie qu'on peut se connecter à une route d'admin en tant qu'administrateur", async () => {
const bearerToken = await createAndLogUser(httpClient, "userAdmin", { type: "ADMIN" })

const response = await httpClient().inject({ method: "GET", path: "/api/admin/appointments/details", headers: bearerToken })
expect(response.statusCode).toBe(200)
})

it.skip("Vérifie qu'on peut se connecter à une route sécurisée en tant que cfa", async () => {
const bearerToken = await createAndLogUser(httpClient, "userCfa", { type: "CFA" })

const response = await httpClient().inject({ method: "GET", path: "/api/authentified", headers: bearerToken })
expect(response.statusCode).toBe(200)
})

it.skip("Vérifie qu'on ne peut pas se connecter à une route d'admin en tant que cfa", async () => {
const bearerToken = await createAndLogUser(httpClient, "userCfa", { type: "CFA" })

const response = await httpClient().inject({ method: "GET", path: "/api/admin/appointments/details", headers: bearerToken })
assert.notStrictEqual(response.statusCode, 200)
})
Expand Down
7 changes: 3 additions & 4 deletions server/tests/utils/login.utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { IUserRecruteur } from "shared/models"

import { UserRecruteur } from "@/common/model"
import { Server } from "@/http/server"
import { createAuthMagicLinkToken } from "@/services/appLinks.service"
import { createUser } from "@/services/userRecruteur.service"

export const createAndLogUser = async (httpClient: () => Server, username: string, options: Partial<IUserRecruteur> = {}) => {
const email = `${username}@mail.com`
const user = await createUser({ username, email, ...options })
const email = `${username.toLowerCase()}@mail.com`
const user = await UserRecruteur.create({ username, email, first_name: "first name", last_name: "last name", ...options })

const response = await httpClient().inject({
method: "POST",
path: "/api/login/verification",
headers: { authorization: `Bearer ${createAuthMagicLinkToken(user)}` },
})

return {
Cookie: response.cookies.reduce((acc, cookie) => `${acc} ${cookie.name}=${cookie.value}`, ""),
}
Expand Down
13 changes: 12 additions & 1 deletion shared/models/usersRecruteur.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const ZUserStatusValidation = z
validation_type: z.enum(["AUTOMATIQUE", "MANUELLE"]).describe("Processus de validation lors de l'inscription de l'utilisateur"),
status: ZEtatUtilisateur.nullish(),
reason: z.string().nullish().describe("Raison du changement de statut"),
user: z.string().describe("Utilisateur ayant effectué la modification | SERVEUR si le compte a été validé automatiquement"),
user: z.string().describe("Id de l'utilisateur ayant effectué la modification | 'SERVEUR' si le compte a été validé automatiquement"),
date: z.date().nullish().describe("Date de l'évènement"),
})
.strict()
Expand Down Expand Up @@ -161,3 +161,14 @@ export const ZAnonymizedUserRecruteur = ZUserRecruteur.pick({
})

export type IAnonymizedUserRecruteur = z.output<typeof ZAnonymizedUserRecruteur>

export const ZUserRecruteurReferentielData = ZUserRecruteur.pick({
establishment_state: true,
is_qualiopi: true,
establishment_siret: true,
establishment_raison_sociale: true,
contacts: true,
address_detail: true,
address: true,
geo_coordinates: true,
}).strict()
10 changes: 5 additions & 5 deletions shared/routes/user.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ export const zUserRecruteurRoutes = {
"/admin/users": {
method: "post",
path: "/admin/users",
// TODO TO BE FIXED
body: z.object({}).passthrough(),
// body: ZUserRecruteur.extend({
// scope: z.string().optional(),
// }).strict(),
body: ZUserRecruteurWritable.omit({
is_email_checked: true,
is_qualiopi: true,
status: true,
}),
response: {
"200": ZUserRecruteur,
},
Expand Down
Loading