From dac5b292bdc25cd517c4039a2ed58ffeb8f21546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A8re=20Pique?= Date: Mon, 10 Feb 2025 17:31:31 +0100 Subject: [PATCH] fix: wip --- .talismanrc | 2 +- .../backend/src/middlewares/trackBoUser.js | 36 +++++++++ packages/backend/src/middlewares/trackEig.js | 36 +++++++++ .../backend/src/middlewares/trackFoUser.js | 36 +++++++++ packages/backend/src/routes/bo-user.js | 12 ++- packages/backend/src/routes/eig.js | 8 ++ packages/backend/src/routes/user.js | 9 ++- packages/backend/src/services/BoUser.js | 68 ++++++++++++++-- packages/backend/src/services/Tracking.js | 58 +++++++++++++ packages/backend/src/services/User.js | 81 ++++++++++++++++--- packages/backend/src/services/eig.js | 46 +++++++++++ .../src/components/user/Compte.vue | 2 +- packages/migrations/package.json | 1 + .../20250130132041_add_tracking_table.js | 34 ++++++++ 14 files changed, 405 insertions(+), 24 deletions(-) create mode 100644 packages/backend/src/middlewares/trackBoUser.js create mode 100644 packages/backend/src/middlewares/trackEig.js create mode 100644 packages/backend/src/middlewares/trackFoUser.js create mode 100644 packages/backend/src/services/Tracking.js create mode 100644 packages/migrations/src/migrations/20250130132041_add_tracking_table.js diff --git a/.talismanrc b/.talismanrc index 8b77e137c..bee8367be 100644 --- a/.talismanrc +++ b/.talismanrc @@ -140,7 +140,7 @@ fileignoreconfig: - filename: packages/backend/src/services/Territoire.js checksum: 9728f0b14665259bf62d6200be40153eb7a8bfaf0a8381e1a0ce4f90087cf59b - filename: packages/backend/src/services/User.js - checksum: 78ddae12d185c4111aa837a178b75e6bda4083d06783ee6382503262298eefbc + checksum: 19d2ccf15e00c2888d48ab27ae7cc0b56fae8ab2d4fff772487dacd805f652b1 - filename: packages/backend/src/services/geo/Commune.js checksum: 40213f6529d1282e73f0201199e28b66f9c99c76d16651d88aac0b59fb722ae2 - filename: packages/backend/src/services/geo/Departement.js diff --git a/packages/backend/src/middlewares/trackBoUser.js b/packages/backend/src/middlewares/trackBoUser.js new file mode 100644 index 000000000..29b8861ce --- /dev/null +++ b/packages/backend/src/middlewares/trackBoUser.js @@ -0,0 +1,36 @@ +const { actions } = require("../services/Tracking"); +const boUser = require("../services/BoUser"); + +function trackFoUser({ action, userType }) { + return async (req, res, next) => { + const { id: userId } = req.decoded; + const { id: boUserId } = req.params; + + let oldUser = null; + if (boUserId) { + oldUser = await boUser.getByUserId(boUserId); + } + + res.on("finish", async () => { + let newEig = null; + + if (action !== actions.deletion) { + newEig = await boUser.getByUserId(boUserId); + } + + if (boUserId) { + boUser.addAsyncEigHistoric({ + action, + boUserId, + data: { newData: newEig, olData: oldUser }, + userId, + userType, + }); + } + }); + + next(); + }; +} + +module.exports = trackFoUser; diff --git a/packages/backend/src/middlewares/trackEig.js b/packages/backend/src/middlewares/trackEig.js new file mode 100644 index 000000000..7a11f33b5 --- /dev/null +++ b/packages/backend/src/middlewares/trackEig.js @@ -0,0 +1,36 @@ +const { actions } = require("../services/Tracking"); +const eigService = require("../services/eig"); + +function trackEig({ action, userType }) { + return async (req, res, next) => { + const { id: userId } = req.decoded; + const { id: eigId } = req.params; + + let oldEig = null; + if (eigId) { + oldEig = await eigService.getByEigId(eigId); + } + + req.on("finish", () => { + let newEig = null; + + if (action !== actions.deletion) { + newEig = eigService.getByEigId(eigId); + } + + if (eigId) { + eigService.addAsyncEigHistoric({ + action, + data: { newData: newEig, olData: oldEig }, + eigId, + userId, + userType, + }); + } + }); + + next(); + }; +} + +module.exports = trackEig; diff --git a/packages/backend/src/middlewares/trackFoUser.js b/packages/backend/src/middlewares/trackFoUser.js new file mode 100644 index 000000000..dec7f8bd8 --- /dev/null +++ b/packages/backend/src/middlewares/trackFoUser.js @@ -0,0 +1,36 @@ +const { actions } = require("../services/Tracking"); +const foUser = require("../services/User"); + +function trackFoUser({ action, userType }) { + return async (req, res, next) => { + const { id: userId } = req.decoded; + const { id: foUserId } = req.params; + + let oldUser = null; + if (foUserId) { + oldUser = await foUser.getByUserId(foUserId); + } + + req.on("finish", async () => { + let newEig = null; + + if (action !== actions.deletion) { + newEig = foUser.getByUserId(foUserId); + } + + if (foUserId) { + foUser.addAsyncEigHistoric({ + action, + data: { newData: newEig, olData: oldUser }, + foUserId, + userId, + userType, + }); + } + }); + + next(); + }; +} + +module.exports = trackFoUser; diff --git a/packages/backend/src/routes/bo-user.js b/packages/backend/src/routes/bo-user.js index f54aa6c8b..5d7e01427 100644 --- a/packages/backend/src/routes/bo-user.js +++ b/packages/backend/src/routes/bo-user.js @@ -7,6 +7,9 @@ const BOcheckRole = require("../middlewares/bo-check-role.js"); const BOUserController = require("../controllers/bo-user"); const checkTerrForAccountCreation = require("../middlewares/bo-check-terr-for-account-creation"); const getDepartements = require("../middlewares/getDepartements"); +const trackBoUser = require("../middlewares/trackBoUser"); + +const { actions, userTypes } = require("../services/Tracking"); const BOcheckRoleCompte = BOcheckRole(["Compte"]); @@ -29,7 +32,12 @@ router.get( // Renvoie les informations liées à l'utilisateur router.get("/:userId", BOcheckJWT, BOcheckRoleCompte, BOUserController.getOne); // Mise à jour de mes informations -router.post("/me", BOcheckJWT, BOUserController.updateMe); +router.post( + "/me", + BOcheckJWT, + trackBoUser({ action: actions.modification, userType: userTypes.back }), + BOUserController.updateMe, +); // Création d'un utilisateur router.post( "/", @@ -37,6 +45,7 @@ router.post( BOcheckRoleCompte, getDepartements, checkTerrForAccountCreation, + trackBoUser({ action: actions.creation, userType: userTypes.back }), BOUserController.create, ); // Mise à jour d'un utilisateur @@ -46,6 +55,7 @@ router.post( BOcheckRoleCompte, getDepartements, checkTerrForAccountCreation, + trackBoUser({ action: actions.modification, userType: userTypes.back }), BOUserController.update, ); // Fonctione transverse de recherche du service compétent diff --git a/packages/backend/src/routes/eig.js b/packages/backend/src/routes/eig.js index ef61c86a9..f156c09d0 100644 --- a/packages/backend/src/routes/eig.js +++ b/packages/backend/src/routes/eig.js @@ -7,6 +7,9 @@ const checkPermissionEIG = require("../middlewares/checkPermissionEIG"); const checkPermissionBOEIG = require("../middlewares/checkPermissionBOEIG"); const boCheckRole = require("../middlewares/bo-check-role"); const boCheckJWT = require("../middlewares/bo-check-JWT"); +const trackEig = require("../middlewares/trackEig"); + +const { actions, userTypes } = require("../services/Tracking"); const { eigController } = require("../controllers"); const getDepartements = require("../middlewares/getDepartements"); @@ -50,6 +53,7 @@ router.post( "/", checkJWT, checkPermissionDeclarationSejourForEig, + trackEig({ action: actions.creation, userType: userTypes.front }), eigController.create, ); router.put( @@ -58,6 +62,7 @@ router.put( checkPermissionEIG, checkPermissionDeclarationSejourForEig, canUpdateEig, + trackEig({ action: actions.modification, userType: userTypes.front }), eigController.update, ); router.post( @@ -65,6 +70,7 @@ router.post( checkJWT, checkPermissionEIG, canUpdateEig, + trackEig({ action: actions.modification, userType: userTypes.front }), eigController.depose, ); @@ -73,6 +79,7 @@ router.delete( checkJWT, checkPermissionEIG, canUpdateEig, + trackEig({ action: actions.deletion, userType: userTypes.front }), eigController.delete, ); @@ -80,6 +87,7 @@ router.post( "/admin/:id/mark-as-read", boCheckJWT, boCheckRoleEig, + trackEig({ action: actions.modification, userType: userTypes.back }), eigController.markAsRead, ); diff --git a/packages/backend/src/routes/user.js b/packages/backend/src/routes/user.js index c9e7e391f..859b3f6f1 100644 --- a/packages/backend/src/routes/user.js +++ b/packages/backend/src/routes/user.js @@ -4,9 +4,16 @@ const router = express.Router(); const checkJWT = require("../middlewares/checkJWT"); const userController = require("../controllers/user"); +const trackFoUser = require("../middlewares/trackFoUser"); +const { actions, userTypes } = require("../services/Tracking"); // Gère une connexion via mot de passe. router.get("/me", checkJWT, userController.getMe); -router.patch("/me", checkJWT, userController.patchMe); +router.patch( + "/me", + checkJWT, + trackFoUser({ action: actions.modification, userType: userTypes.front }), + userController.patchMe, +); module.exports = router; diff --git a/packages/backend/src/services/BoUser.js b/packages/backend/src/services/BoUser.js index b25431abf..760ee3440 100644 --- a/packages/backend/src/services/BoUser.js +++ b/packages/backend/src/services/BoUser.js @@ -1,3 +1,6 @@ +const Sentry = require("@sentry/node"); + +const { sentry } = require("../config"); const logger = require("../utils/logger"); const pool = require("../utils/pgpool").getPool(); const normalize = require("../utils/normalize"); @@ -11,6 +14,8 @@ const { const AppError = require("../utils/error"); +const { addHistoric, entities } = require("./Tracking"); + const log = logger(module.filename); const query = { @@ -348,7 +353,6 @@ module.exports.updateMe = async (id, { nom, prenom }) => { statusCode: 500, }); } - // Mise à jour du compte en base de données const { rowCount } = await pool.query(...query.updateMe(id, nom, prenom)); if (rowCount === 0) { @@ -431,13 +435,13 @@ module.exports.editStatus = async (userId, isBlocked) => { module.exports.activate = async (email) => { log.i("active - IN", { email }); - let response = await pool.query( + const response = await pool.query( ...query.get({ "us.mail": normalize(email) }), ); if (response.rowCount === 0) { throw new AppError("Utilisateur non trouvé", { name: "UserNotFound" }); } - let user = response.rows[0]; + const user = response.rows[0]; log.d("activate", { user }); if (user.validate) { throw new AppError("Utilisateur déjà actif", { @@ -448,10 +452,13 @@ module.exports.activate = async (email) => { await pool.query(query.activate, [user.id]); // TODO: remove - response = await pool.query(...query.get({ "us.mail": normalize(email) })); - [user] = response.rows; - log.i("active - DONE", { user }); - return user; + const responseWithUpdate = await pool.query( + ...query.get({ "us.mail": normalize(email) }), + ); + const [userUpdated] = responseWithUpdate.rows; + + log.i("active - DONE", { userUpdated }); + return userUpdated; }; module.exports.read = async ( @@ -772,3 +779,50 @@ module.exports.login = async ({ email, password }) => { log.i("login - DONE"); return user.rows[0]; }; + +const getByUserId = async (userId) => { + try { + const params = { + "us.id": userId, + }; + const response = await pool.query(...query.get(params)); + return response.rows[0]; + } catch (error) { + log.w("getByUserId - DONE with error", error); + if (sentry.enabled) { + Sentry.captureException(error); + } + return null; + } +}; + +module.exports.getByUserId = getByUserId; + +const addAsyncUserHistoric = async ({ + data: { oldData, newData }, + boUserId, + userId, + action, + userType, +}) => { + try { + addHistoric({ + action, + data: { + after: newData, + before: oldData, + }, + entity: entities.userBack, + entityId: boUserId, + userId, + userType, + }); + } catch (error) { + log.w("addAsyncHistoric - DONE with error", error); + if (sentry.enabled) { + Sentry.captureException(error); + } + } +}; + +module.exports.addAsyncUserHistoric = addAsyncUserHistoric; diff --git a/packages/backend/src/services/Tracking.js b/packages/backend/src/services/Tracking.js new file mode 100644 index 000000000..68b7dc7dd --- /dev/null +++ b/packages/backend/src/services/Tracking.js @@ -0,0 +1,58 @@ +const pool = require("../utils/pgpool").getPool(); + +const actions = { + creation: "CREATION", + deactivation: "DEACTIVATION", + deletion: "DELETION", + modification: "MODIFICATION", +}; + +module.exports.actions = actions; + +const entities = { + eig: "EIG", + userBack: "USER_BACK", + userFront: "USER_FRONT", +}; + +module.exports.entities = entities; + +const userTypes = { + back: "BACK", + front: "FRONT", +}; + +module.exports.userTypes = userTypes; + +const query = { + add: ` + INSERT INTO tracking_actions (entity, entity_id, action, data, user_id, user_type) + VALUES ($1, $2, $3, $4, $5 ,$6) + `, +}; + +module.exports.addHistoric = async ({ + entity, + entityId, + action, + data = null, + userId, + userType, +}) => { + if (!action || !entity || !userType) { + throw new Error(`${module.filename}: Invalid action, entity or user type`, { + action, + entity, + userType, + }); + } + + await pool.query(query.add, [ + entity, + entityId, + action, + data, + userId, + userType, + ]); +}; diff --git a/packages/backend/src/services/User.js b/packages/backend/src/services/User.js index c5cdd3c6d..21b80ec1b 100644 --- a/packages/backend/src/services/User.js +++ b/packages/backend/src/services/User.js @@ -1,9 +1,13 @@ +const Sentry = require("@sentry/node"); + +const { sentry } = require("../config"); const logger = require("../utils/logger"); const pool = require("../utils/pgpool").getPool(); const normalize = require("../utils/normalize"); const AppError = require("../utils/error"); const { status } = require("../helpers/users"); +const { addHistoric, entities, userTypes } = require("./Tracking"); const log = logger(module.filename); @@ -182,14 +186,18 @@ module.exports.registerByEmail = async ({ module.exports.editPassword = async ({ email, password }) => { log.i("editPassword - IN", { email }); - let response = await pool.query(...query.select({ mail: normalize(email) })); - let [user] = response.rows; + const response = await pool.query( + ...query.select({ mail: normalize(email) }), + ); + const [user] = response.rows; log.d("editPassword", { user }); await pool.query(...query.editPassword(email, password)); - response = await pool.query(...query.select({ mail: normalize(email) })); - [user] = response.rows; - log.i("editPassword - DONE", { user }); - return user; + const responseWithUpdate = await pool.query( + ...query.select({ mail: normalize(email) }), + ); + const [userUpdated] = responseWithUpdate.rows; + log.i("editPassword - DONE", { user: userUpdated }); + return userUpdated; }; module.exports.editStatus = async (userId, statusCode) => { @@ -200,11 +208,13 @@ module.exports.editStatus = async (userId, statusCode) => { module.exports.activate = async (email) => { log.i("active - IN", { email }); - let response = await pool.query(...query.select({ mail: normalize(email) })); + const response = await pool.query( + ...query.select({ mail: normalize(email) }), + ); if (response.rows.length === 0) { throw new AppError("Utilisateur non trouvé", { name: "UserNotFound" }); } - let user = response.rows[0]; + const user = response.rows[0]; log.d("activate", { user }); if (!user.sub && user.statusCode !== status.NEED_EMAIL_VALIDATION) { throw new AppError("Utilisateur déjà actif", { @@ -216,9 +226,12 @@ module.exports.activate = async (email) => { } await pool.query(query.activate, [user.id]); - response = await pool.query(...query.select({ mail: normalize(email) })); - [user] = response.rows; - log.i("active - DONE", { user }); + const responseWithUpdate = await pool.query( + ...query.select({ mail: normalize(email) }), + ); + const [userUpdated] = responseWithUpdate.rows; + + log.i("active - DONE", { user: userUpdated }); return user; }; @@ -242,6 +255,48 @@ module.exports.login = async ({ email, password }) => { }; module.exports.updateUser = async ({ id, nom, prenom }) => { - const users = await pool.query(query.updateUser, [id, nom, prenom]); - return users.rows[0]; + const response = await pool.query(query.updateUser, [id, nom, prenom]); + return response.rows[0]; +}; + +const getByUserId = async (userId) => { + try { + const response = await pool.query(...query.select({ id: userId })); + return response.rows[0]; + } catch (error) { + log.w("getByUserId - DONE with error", error); + if (sentry.enabled) { + Sentry.captureException(error); + } + return null; + } +}; + +module.exports.getByUserId = getByUserId; + +module.exports.addAsyncEigHistoric = async ({ + data: { oldData, newData }, + foUserId, + userId, + action, + userType, +}) => { + try { + addHistoric({ + action, + data: { + after: newData, + before: oldData, + }, + entity: entities.userFront, + entityId: userId, + userId: foUserId, + userType: userType ?? userTypes.front, + }); + } catch (error) { + log.w("addAsyncHistoric - DONE with error", error); + if (sentry.enabled) { + Sentry.captureException(error); + } + } }; diff --git a/packages/backend/src/services/eig.js b/packages/backend/src/services/eig.js index 8d93b4044..0bdc2f3f0 100644 --- a/packages/backend/src/services/eig.js +++ b/packages/backend/src/services/eig.js @@ -1,7 +1,11 @@ +const Sentry = require("@sentry/node"); + +const { sentry } = require("../config"); const { statuts, Types, Categorie } = require("../helpers/eig"); const logger = require("../utils/logger"); const AppError = require("../utils/error"); const pool = require("../utils/pgpool").getPool(); +const { addHistoric, entities, userTypes } = require("./Tracking"); const log = logger(module.filename); @@ -665,3 +669,45 @@ module.exports.getAvailableDs = async (organismeId, search) => { ]); return data; }; + +const getByEigId = async (eigId) => { + try { + const response = await pool.query(query.getById, [eigId]); + return response.rows[0]; + } catch (error) { + log.w("getByEigId - DONE with error", error); + if (sentry.enabled) { + Sentry.captureException(error); + } + return null; + } +}; + +module.exports.getByEigId = getByEigId; + +module.exports.addAsyncEigHistoric = async ({ + data: { oldData, newData }, + eigId, + userId, + action, + userType, +}) => { + try { + addHistoric({ + action, + data: { + after: newData, + before: oldData, + }, + entity: entities.eig, + entityId: eigId, + userId, + userType: userType ?? userTypes.front, + }); + } catch (error) { + log.w("addAsyncHistoric - DONE with error", error); + if (sentry.enabled) { + Sentry.captureException(error); + } + } +}; diff --git a/packages/frontend-bo/src/components/user/Compte.vue b/packages/frontend-bo/src/components/user/Compte.vue index 521b4d934..bbab8cea1 100644 --- a/packages/frontend-bo/src/components/user/Compte.vue +++ b/packages/frontend-bo/src/components/user/Compte.vue @@ -9,7 +9,7 @@
} + */ +exports.up = (knex) => + knex.schema + .withSchema("public") + .createTable("tracking_actions", (table) => { + table.increments("id").primary(); + table.string("entity").notNullable(); + table.text("entity_id").notNullable(); + table.string("action", 50).notNullable(); + table.jsonb("data"); + table.integer("user_id").notNullable(); + table.string("user_type", 50).notNullable(); + table.timestamp("timestamp", { useTz: true }).defaultTo(knex.fn.now()); + }) + .then(function () { + return knex.raw( + "GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.tracking_actions TO vao_u", + ); + }) + .then(function () { + return knex.raw( + "GRANT USAGE, SELECT ON SEQUENCE public.tracking_actions_id_seq TO vao_u", + ); + }); + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = (knex) => + knex.schema.withSchema("public").dropTableIfExists("tracking_actions");