diff --git a/backend/package-lock.json b/backend/package-lock.json index 42f6889..4a8fe56 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@electric-sql/pglite": "^0.2.17", + "@types/morgan": "^1.9.10", "@types/multer": "^1.4.12", "axios": "^1.8.4", "bcryptjs": "^3.0.2", @@ -28,6 +29,7 @@ "jsdom": "^26.1.0", "json2csv": "^6.0.0-alpha.2", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", "multer": "^2.1.1", "nodemailer": "^8.0.3", "papaparse": "^5.5.2", @@ -795,6 +797,15 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/multer": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", @@ -996,6 +1007,24 @@ ], "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", @@ -2692,6 +2721,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2922,6 +2994,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index 5f7e660..b0d278c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,6 +20,7 @@ "description": "", "dependencies": { "@electric-sql/pglite": "^0.2.17", + "@types/morgan": "^1.9.10", "@types/multer": "^1.4.12", "axios": "^1.8.4", "bcryptjs": "^3.0.2", @@ -38,6 +39,7 @@ "jsdom": "^26.1.0", "json2csv": "^6.0.0-alpha.2", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", "multer": "^2.1.1", "nodemailer": "^8.0.3", "papaparse": "^5.5.2", diff --git a/backend/server.ts b/backend/server.ts index e198b12..f492883 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -26,6 +26,8 @@ import { initRoles } from './src/database/initdb/initrole' import {initEvent} from './src/database/initdb/initevent' import {initChallenge} from './src/database/initdb/initChallenge' import { authenticateUser } from './src/middlewares/auth.middleware'; +import { requestLogger } from './src/middlewares/request-logger.middleware'; +import { errorLogger } from './src/middlewares/error-logger.middleware'; dotenv.config(); @@ -36,6 +38,7 @@ async function startServer() { app.use(cors({ origin: "*" })); app.use(bodyParser.json()); app.use(express.urlencoded({ extended: true })); + app.use(requestLogger); try { @@ -67,6 +70,7 @@ async function startServer() { app.use("/api/uploads/foodmenu", express.static(path.join(__dirname, "/uploads/foodmenu"))); app.use("/api/uploads/plannings", express.static(path.join(__dirname, "/uploads/plannings"))); app.use("/api/exports/bus", express.static(path.join(__dirname, "/exports/bus"))); + app.use(errorLogger); // Démarrage du serveur app.listen(server_port, () => { @@ -78,4 +82,4 @@ async function startServer() { } } -startServer(); \ No newline at end of file +startServer(); diff --git a/backend/src/middlewares/error-logger.middleware.ts b/backend/src/middlewares/error-logger.middleware.ts new file mode 100644 index 0000000..d320a1a --- /dev/null +++ b/backend/src/middlewares/error-logger.middleware.ts @@ -0,0 +1,21 @@ +import { ErrorRequestHandler } from 'express'; + +export const errorLogger: ErrorRequestHandler = (err, req, _res, next) => { + const statusCode = + typeof err?.statusCode === 'number' + ? err.statusCode + : typeof err?.status === 'number' + ? err.status + : 500; + + console.error('[request-error]', { + timestamp: new Date().toISOString(), + method: req.method, + url: req.originalUrl, + statusCode, + message: err?.message || 'Unknown error', + stack: err?.stack + }); + + next(err); +}; diff --git a/backend/src/middlewares/request-logger.middleware.ts b/backend/src/middlewares/request-logger.middleware.ts new file mode 100644 index 0000000..6e3ca88 --- /dev/null +++ b/backend/src/middlewares/request-logger.middleware.ts @@ -0,0 +1,47 @@ +import { Request } from 'express'; +import morgan from 'morgan'; + +function getUserEmail(req: Request): string { + const user = req?.user; + + if (!user || typeof user === 'string') { + return '-'; + } + + const maybeEmail = (user as Record)['userEmail']; + return typeof maybeEmail === 'string' && maybeEmail.length > 0 ? maybeEmail : '-'; +} + +function getClientIp(req: Request): string { + const forwardedFor = req.headers['x-forwarded-for']; + + if (typeof forwardedFor === 'string' && forwardedFor.length > 0) { + return forwardedFor.split(',')[0].trim(); + } + + if (Array.isArray(forwardedFor) && forwardedFor.length > 0) { + return forwardedFor[0]; + } + + return req.ip || req.socket.remoteAddress || 'unknown'; +} + +morgan.token('client-ip', (req: Request) => getClientIp(req)); +morgan.token('user-email', (req: Request) => getUserEmail(req)); +morgan.token('statusColor', (req, res, args) => { + // get the status code if response written + var status = res.statusCode ?? undefined + + // get status color + var color = status >= 500 ? 31 // red + : status >= 400 ? 33 // yellow + : status >= 300 ? 36 // cyan + : status >= 200 ? 32 // green + : 0; // no color + + return '\x1b[' + color + 'm' + status + '\x1b[0m'; +}); + +export const requestLogger = morgan( + '\x1b[33m:method :url :statusColor :response-time ms - :client-ip - :user-email', +);