diff --git a/.gitignore b/.gitignore index f4e61e4..6b11e04 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +*.log +logs/ # Editor directories and files .idea diff --git a/backend/middleware/apiLogger.js b/backend/middleware/apiLogger.js new file mode 100644 index 0000000..af0df5d --- /dev/null +++ b/backend/middleware/apiLogger.js @@ -0,0 +1,53 @@ +const logger = require('../utils/logger'); + +const apiLogger = (req, res, next) => { + const startTime = Date.now(); + const reqLogger = logger.withRequestId(req.id); + + const requestData = { + method: req.method, + url: req.url, + userAgent: req.get('User-Agent'), + ip: req.ip, + userId: req.user?.id || 'anonymous', + body: logger.sanitize(req.body), + params: req.params, + query: req.query, + headers: { + 'content-type': req.get('Content-Type'), + 'content-length': req.get('Content-Length'), + 'referer': req.get('Referer'), + } + }; + + reqLogger.info('API Request', requestData); + + const originalSend = res.send; + res.send = function(body) { + const endTime = Date.now(); + const responseTime = endTime - startTime; + + const responseData = { + statusCode: res.statusCode, + responseTime: `${responseTime}ms`, + contentLength: res.get('Content-Length') || (body ? body.length : 0), + method: req.method, + url: req.url, + userId: req.user?.id || 'anonymous' + }; + + if (res.statusCode >= 400 && res.statusCode < 500) { + reqLogger.warn('API Response - Client Error', responseData); + } else if (res.statusCode >= 500) { + reqLogger.error('API Response - Server Error', responseData); + } else { + reqLogger.info('API Response - Success', responseData); + } + + return originalSend.call(this, body); + }; + + next(); +}; + +module.exports = apiLogger; \ No newline at end of file diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 4f51c6c..f532b46 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -1,5 +1,6 @@ const jwt = require("jsonwebtoken"); const { User } = require("../models"); // Import from models/index.js to get models with associations +const logger = require("../utils/logger"); const authenticateToken = async (req, res, next) => { // First try to get token from cookie @@ -43,7 +44,13 @@ const authenticateToken = async (req, res, next) => { }); } - console.error("Auth middleware error:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Auth middleware error", { + error: error.message, + stack: error.stack, + tokenPresent: !!token, + userId: req.user?.id + }); return res.status(403).json({ error: "Invalid token", code: "INVALID_TOKEN", diff --git a/backend/middleware/errorLogger.js b/backend/middleware/errorLogger.js new file mode 100644 index 0000000..d9f81c1 --- /dev/null +++ b/backend/middleware/errorLogger.js @@ -0,0 +1,33 @@ +const logger = require('../utils/logger'); + +const errorLogger = (err, req, res, next) => { + // Create a request-specific logger with request ID + const reqLogger = logger.withRequestId(req.id); + + // Log the error with context + const errorContext = { + method: req.method, + url: req.url, + userAgent: req.get('User-Agent'), + ip: req.ip, + userId: req.user?.id || 'anonymous', + body: logger.sanitize(req.body), + params: req.params, + query: req.query, + statusCode: err.statusCode || 500, + stack: err.stack, + }; + + if (err.statusCode && err.statusCode < 500) { + // Client errors (4xx) + reqLogger.warn(`Client error: ${err.message}`, errorContext); + } else { + // Server errors (5xx) + reqLogger.error(`Server error: ${err.message}`, errorContext); + } + + // Pass error to next middleware + next(err); +}; + +module.exports = errorLogger; \ No newline at end of file diff --git a/backend/middleware/security.js b/backend/middleware/security.js index 4eb0d71..ec83a26 100644 --- a/backend/middleware/security.js +++ b/backend/middleware/security.js @@ -1,3 +1,5 @@ +const logger = require('../utils/logger'); + // HTTPS enforcement middleware const enforceHTTPS = (req, res, next) => { // Skip HTTPS enforcement in development @@ -20,11 +22,13 @@ const enforceHTTPS = (req, res, next) => { // Log the redirect for monitoring if (req.headers.host !== allowedHost) { - console.warn("[SECURITY] Host header mismatch during HTTPS redirect:", { + const reqLogger = logger.withRequestId(req.id); + reqLogger.warn("Host header mismatch during HTTPS redirect", { requestHost: req.headers.host, allowedHost, ip: req.ip, url: req.url, + eventType: 'SECURITY_HOST_MISMATCH' }); } @@ -70,34 +74,21 @@ const addRequestId = (req, res, next) => { // Log security events const logSecurityEvent = (eventType, details, req) => { + const reqLogger = logger.withRequestId(req.id || "unknown"); + const logEntry = { - timestamp: new Date().toISOString(), eventType, - requestId: req.id || "unknown", ip: req.ip || req.connection.remoteAddress, userAgent: req.get("user-agent"), userId: req.user?.id || "anonymous", ...details, }; - // In production, this should write to a secure log file or service - if (process.env.NODE_ENV === "production") { - console.log("[SECURITY]", JSON.stringify(logEntry)); - } else { - console.log("[SECURITY]", eventType, details); - } + reqLogger.warn(`Security event: ${eventType}`, logEntry); }; // Sanitize error messages to prevent information leakage const sanitizeError = (err, req, res, next) => { - // Log the full error internally - console.error("Error:", { - requestId: req.id, - error: err.message, - stack: err.stack, - userId: req.user?.id, - }); - // Send sanitized error to client const isDevelopment = process.env.NODE_ENV === "dev" || process.env.NODE_ENV === "development"; diff --git a/backend/package-lock.json b/backend/package-lock.json index 062c7ec..55f8061 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,13 +24,16 @@ "helmet": "^8.1.0", "jsdom": "^27.0.0", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", "multer": "^2.0.2", "node-cron": "^3.0.3", "pg": "^8.16.3", "sequelize": "^6.37.7", "sequelize-cli": "^6.6.3", "stripe": "^18.4.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@types/jest": "^30.0.0", @@ -607,6 +610,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -739,6 +751,17 @@ "node": ">=18" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@googlemaps/google-maps-services-js": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.4.2.tgz", @@ -1405,6 +1428,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -1570,6 +1599,12 @@ "dev": true, "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1731,6 +1766,24 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "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", @@ -2163,6 +2216,16 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2179,6 +2242,41 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2603,6 +2701,12 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2897,6 +3001,12 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -2920,6 +3030,15 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2971,6 +3090,12 @@ "node": ">=8" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -3615,6 +3740,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3704,7 +3835,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4733,6 +4863,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -4803,6 +4939,23 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5002,6 +5155,49 @@ "node": "*" } }, + "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", @@ -5285,6 +5481,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5307,6 +5512,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", @@ -5315,6 +5529,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -5972,6 +6195,15 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6249,6 +6481,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -6366,6 +6607,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -6732,6 +6982,12 @@ "node": "*" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/tldts": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz", @@ -6815,6 +7071,15 @@ "node": ">=18" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -7120,6 +7385,60 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", diff --git a/backend/package.json b/backend/package.json index 486811a..c091d76 100644 --- a/backend/package.json +++ b/backend/package.json @@ -36,13 +36,16 @@ "helmet": "^8.1.0", "jsdom": "^27.0.0", "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", "multer": "^2.0.2", "node-cron": "^3.0.3", "pg": "^8.16.3", "sequelize": "^6.37.7", "sequelize-cli": "^6.6.3", "stripe": "^18.4.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@types/jest": "^30.0.0", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 4d77e08..358abcf 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -2,6 +2,7 @@ const express = require("express"); const jwt = require("jsonwebtoken"); const { OAuth2Client } = require("google-auth-library"); const { User } = require("../models"); // Import from models/index.js to get models with associations +const logger = require("../utils/logger"); const { sanitizeInput, validateRegistration, @@ -84,6 +85,13 @@ router.post( maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User registration successful", { + userId: user.id, + username: user.username, + email: user.email + }); + res.status(201).json({ user: { id: user.id, @@ -95,7 +103,13 @@ router.post( // Don't send token in response body for security }); } catch (error) { - console.error("Registration error:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Registration error", { + error: error.message, + stack: error.stack, + email: req.body.email, + username: req.body.username + }); res.status(500).json({ error: "Registration failed. Please try again." }); } } @@ -164,6 +178,12 @@ router.post( maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User login successful", { + userId: user.id, + email: user.email + }); + res.json({ user: { id: user.id, @@ -175,7 +195,12 @@ router.post( // Don't send token in response body for security }); } catch (error) { - console.error("Login error:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Login error", { + error: error.message, + stack: error.stack, + email: req.body.email + }); res.status(500).json({ error: "Login failed. Please try again." }); } } @@ -271,6 +296,13 @@ router.post( maxAge: 7 * 24 * 60 * 60 * 1000, }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Google authentication successful", { + userId: user.id, + email: user.email, + isNewUser: !user.createdAt || (Date.now() - new Date(user.createdAt).getTime()) < 1000 + }); + res.json({ user: { id: user.id, @@ -298,7 +330,12 @@ router.post( .status(400) .json({ error: "Malformed Google token. Please try again." }); } - console.error("Google auth error:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Google auth error", { + error: error.message, + stack: error.stack, + tokenInfo: logger.sanitize({ idToken: req.body.idToken }) + }); res .status(500) .json({ error: "Google authentication failed. Please try again." }); @@ -341,6 +378,11 @@ router.post("/refresh", async (req, res) => { maxAge: 15 * 60 * 1000, }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Token refresh successful", { + userId: user.id + }); + res.json({ user: { id: user.id, @@ -351,13 +393,23 @@ router.post("/refresh", async (req, res) => { }, }); } catch (error) { - console.error("Token refresh error:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Token refresh error", { + error: error.message, + stack: error.stack, + userId: req.user?.id + }); res.status(401).json({ error: "Invalid or expired refresh token" }); } }); // Logout endpoint router.post("/logout", (req, res) => { + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User logout", { + userId: req.user?.id || 'anonymous' + }); + // Clear cookies res.clearCookie("accessToken"); res.clearCookie("refreshToken"); diff --git a/backend/routes/itemRequests.js b/backend/routes/itemRequests.js index 4826d5b..d5af823 100644 --- a/backend/routes/itemRequests.js +++ b/backend/routes/itemRequests.js @@ -2,6 +2,7 @@ const express = require('express'); const { Op } = require('sequelize'); const { ItemRequest, ItemRequestResponse, User, Item } = require('../models'); const { authenticateToken } = require('../middleware/auth'); +const logger = require('../utils/logger'); const router = express.Router(); router.get('/', async (req, res) => { @@ -38,6 +39,15 @@ router.get('/', async (req, res) => { order: [['createdAt', 'DESC']] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item requests fetched", { + search, + status, + requestsCount: count, + page: parseInt(page), + limit: parseInt(limit) + }); + res.json({ requests: rows, totalPages: Math.ceil(count / limit), @@ -45,6 +55,12 @@ router.get('/', async (req, res) => { totalRequests: count }); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item requests fetch failed", { + error: error.message, + stack: error.stack, + query: req.query + }); res.status(500).json({ error: error.message }); } }); @@ -78,8 +94,20 @@ router.get('/my-requests', authenticateToken, async (req, res) => { order: [['createdAt', 'DESC']] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User item requests fetched", { + userId: req.user.id, + requestsCount: requests.length + }); + res.json(requests); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("User item requests fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -115,8 +143,20 @@ router.get('/:id', async (req, res) => { return res.status(404).json({ error: 'Item request not found' }); } + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item request fetched", { + requestId: req.params.id, + requesterId: request.requesterId + }); + res.json(request); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item request fetch failed", { + error: error.message, + stack: error.stack, + requestId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -138,8 +178,22 @@ router.post('/', authenticateToken, async (req, res) => { ] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item request created", { + requestId: request.id, + requesterId: req.user.id, + title: req.body.title + }); + res.status(201).json(requestWithRequester); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item request creation failed", { + error: error.message, + stack: error.stack, + requesterId: req.user.id, + requestData: logger.sanitize(req.body) + }); res.status(500).json({ error: error.message }); } }); @@ -168,8 +222,21 @@ router.put('/:id', authenticateToken, async (req, res) => { ] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item request updated", { + requestId: req.params.id, + requesterId: req.user.id + }); + res.json(updatedRequest); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item request update failed", { + error: error.message, + stack: error.stack, + requestId: req.params.id, + requesterId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -187,8 +254,22 @@ router.delete('/:id', authenticateToken, async (req, res) => { } await request.destroy(); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item request deleted", { + requestId: req.params.id, + requesterId: req.user.id + }); + res.status(204).send(); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item request deletion failed", { + error: error.message, + stack: error.stack, + requestId: req.params.id, + requesterId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -231,8 +312,22 @@ router.post('/:id/responses', authenticateToken, async (req, res) => { ] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item request response created", { + requestId: req.params.id, + responseId: response.id, + responderId: req.user.id + }); + res.status(201).json(responseWithDetails); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item request response creation failed", { + error: error.message, + stack: error.stack, + requestId: req.params.id, + responderId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -277,8 +372,23 @@ router.put('/responses/:responseId/status', authenticateToken, async (req, res) ] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item request response status updated", { + responseId: req.params.responseId, + newStatus: status, + requesterId: req.user.id, + requestFulfilled: status === 'accepted' + }); + res.json(updatedResponse); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item request response status update failed", { + error: error.message, + stack: error.stack, + responseId: req.params.responseId, + requesterId: req.user.id + }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/items.js b/backend/routes/items.js index 813da8e..6fe8fd5 100644 --- a/backend/routes/items.js +++ b/backend/routes/items.js @@ -2,6 +2,7 @@ const express = require("express"); const { Op } = require("sequelize"); const { Item, User, Rental } = require("../models"); // Import from models/index.js to get models with associations const { authenticateToken } = require("../middleware/auth"); +const logger = require("../utils/logger"); const router = express.Router(); router.get("/", async (req, res) => { @@ -60,6 +61,14 @@ router.get("/", async (req, res) => { return itemData; }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Items search completed", { + filters: { minPrice, maxPrice, city, zipCode, search }, + resultsCount: count, + page: parseInt(page), + limit: parseInt(limit) + }); + res.json({ items: itemsWithRoundedCoords, totalPages: Math.ceil(count / limit), @@ -67,6 +76,12 @@ router.get("/", async (req, res) => { totalItems: count, }); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Items search failed", { + error: error.message, + stack: error.stack, + query: req.query + }); res.status(500).json({ error: error.message }); } }); @@ -87,8 +102,20 @@ router.get("/recommendations", authenticateToken, async (req, res) => { order: [["createdAt", "DESC"]], }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Recommendations fetched", { + userId: req.user.id, + recommendationsCount: recommendations.length + }); + res.json(recommendations); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Recommendations fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -120,12 +147,25 @@ router.get('/:id/reviews', async (req, res) => { ? reviews.reduce((sum, review) => sum + review.itemRating, 0) / reviews.length : 0; + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item reviews fetched", { + itemId: req.params.id, + reviewsCount: reviews.length, + averageRating + }); + res.json({ reviews, averageRating, totalReviews: reviews.length }); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item reviews fetch failed", { + error: error.message, + stack: error.stack, + itemId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -155,8 +195,20 @@ router.get("/:id", async (req, res) => { itemResponse.longitude = Math.round(parseFloat(itemResponse.longitude) * 100) / 100; } + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item fetched", { + itemId: req.params.id, + ownerId: item.ownerId + }); + res.json(itemResponse); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item fetch failed", { + error: error.message, + stack: error.stack, + itemId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -178,8 +230,22 @@ router.post("/", authenticateToken, async (req, res) => { ], }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item created", { + itemId: item.id, + ownerId: req.user.id, + itemName: req.body.name + }); + res.status(201).json(itemWithOwner); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item creation failed", { + error: error.message, + stack: error.stack, + ownerId: req.user.id, + itemData: logger.sanitize(req.body) + }); res.status(500).json({ error: error.message }); } }); @@ -208,8 +274,21 @@ router.put("/:id", authenticateToken, async (req, res) => { ], }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item updated", { + itemId: req.params.id, + ownerId: req.user.id + }); + res.json(updatedItem); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item update failed", { + error: error.message, + stack: error.stack, + itemId: req.params.id, + ownerId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -227,8 +306,22 @@ router.delete("/:id", authenticateToken, async (req, res) => { } await item.destroy(); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Item deleted", { + itemId: req.params.id, + ownerId: req.user.id + }); + res.status(204).send(); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Item deletion failed", { + error: error.message, + stack: error.stack, + itemId: req.params.id, + ownerId: req.user.id + }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/maps.js b/backend/routes/maps.js index b9592e5..65d526c 100644 --- a/backend/routes/maps.js +++ b/backend/routes/maps.js @@ -3,6 +3,7 @@ const router = express.Router(); const { authenticateToken } = require("../middleware/auth"); const rateLimiter = require("../middleware/rateLimiter"); const googleMapsService = require("../services/googleMapsService"); +const logger = require("../utils/logger"); // Input validation middleware const validateInput = (req, res, next) => { @@ -34,8 +35,12 @@ const validateInput = (req, res, next) => { }; // Error handling middleware -const handleServiceError = (error, res) => { - console.error("Maps service error:", error.message); +const handleServiceError = (error, res, req) => { + const reqLogger = logger.withRequestId(req?.id); + reqLogger.error("Maps service error", { + error: error.message, + stack: error.stack + }); if (error.message.includes("API key not configured")) { return res.status(503).json({ @@ -87,17 +92,16 @@ router.post( ); // Log request for monitoring (without sensitive data) - console.log( - `Places Autocomplete: user=${ - req.user?.id || "anonymous" - }, query_length=${input.length}, results=${ - result.predictions?.length || 0 - }` - ); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Places Autocomplete request", { + userId: req.user?.id || "anonymous", + queryLength: input.length, + resultsCount: result.predictions?.length || 0 + }); res.json(result); } catch (error) { - handleServiceError(error, res); + handleServiceError(error, res, req); } } ); @@ -127,15 +131,15 @@ router.post( const result = await googleMapsService.getPlaceDetails(placeId, options); // Log request for monitoring - console.log( - `Place Details: user=${ - req.user?.id || "anonymous" - }, placeId=${placeId.substring(0, 10)}...` - ); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Place Details request", { + userId: req.user?.id || "anonymous", + placeIdPrefix: placeId.substring(0, 10) + "..." + }); res.json(result); } catch (error) { - handleServiceError(error, res); + handleServiceError(error, res, req); } } ); @@ -165,15 +169,15 @@ router.post( const result = await googleMapsService.geocodeAddress(address, options); // Log request for monitoring - console.log( - `Geocoding: user=${req.user?.id || "anonymous"}, address_length=${ - address.length - }` - ); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Geocoding request", { + userId: req.user?.id || "anonymous", + addressLength: address.length + }); res.json(result); } catch (error) { - handleServiceError(error, res); + handleServiceError(error, res, req); } } ); diff --git a/backend/routes/messages.js b/backend/routes/messages.js index e15ac50..507b675 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -1,6 +1,7 @@ const express = require('express'); const { Message, User } = require('../models'); const { authenticateToken } = require('../middleware/auth'); +const logger = require('../utils/logger'); const router = express.Router(); // Get all messages for the current user (inbox) @@ -17,8 +18,21 @@ router.get('/', authenticateToken, async (req, res) => { ], order: [['createdAt', 'DESC']] }); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Messages inbox fetched", { + userId: req.user.id, + messageCount: messages.length + }); + res.json(messages); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Messages inbox fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -37,8 +51,21 @@ router.get('/sent', authenticateToken, async (req, res) => { ], order: [['createdAt', 'DESC']] }); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Sent messages fetched", { + userId: req.user.id, + messageCount: messages.length + }); + res.json(messages); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Sent messages fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -86,8 +113,22 @@ router.get('/:id', authenticateToken, async (req, res) => { await message.update({ isRead: true }); } + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Message fetched", { + userId: req.user.id, + messageId: req.params.id, + markedAsRead: message.receiverId === req.user.id && !message.isRead + }); + res.json(message); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Message fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + messageId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -124,8 +165,23 @@ router.post('/', authenticateToken, async (req, res) => { }] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Message sent", { + senderId: req.user.id, + receiverId: receiverId, + messageId: message.id, + isReply: !!parentMessageId + }); + res.status(201).json(messageWithSender); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Message send failed", { + error: error.message, + stack: error.stack, + senderId: req.user.id, + receiverId: req.body.receiverId + }); res.status(500).json({ error: error.message }); } }); @@ -145,8 +201,22 @@ router.put('/:id/read', authenticateToken, async (req, res) => { } await message.update({ isRead: true }); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Message marked as read", { + userId: req.user.id, + messageId: req.params.id + }); + res.json(message); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Message mark as read failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + messageId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -160,8 +230,20 @@ router.get('/unread/count', authenticateToken, async (req, res) => { isRead: false } }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Unread message count fetched", { + userId: req.user.id, + unreadCount: count + }); + res.json({ count }); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Unread message count fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js index 7145438..5a5815e 100644 --- a/backend/routes/rentals.js +++ b/backend/routes/rentals.js @@ -4,6 +4,7 @@ const { Rental, Item, User } = require("../models"); // Import from models/index const { authenticateToken } = require("../middleware/auth"); const FeeCalculator = require("../utils/feeCalculator"); const RefundService = require("../services/refundService"); +const logger = require("../utils/logger"); const router = express.Router(); // Helper function to check and update review visibility @@ -67,7 +68,12 @@ router.get("/my-rentals", authenticateToken, async (req, res) => { res.json(rentals); } catch (error) { - console.error("Error in my-rentals route:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error in my-rentals route", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: "Failed to fetch rentals" }); } }); @@ -90,7 +96,12 @@ router.get("/my-listings", authenticateToken, async (req, res) => { res.json(rentals); } catch (error) { - console.error("Error in my-listings route:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error in my-listings route", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: "Failed to fetch listings" }); } }); @@ -125,7 +136,13 @@ router.get("/:id", authenticateToken, async (req, res) => { res.json(rental); } catch (error) { - console.error("Error fetching rental:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error fetching rental", { + error: error.message, + stack: error.stack, + rentalId: req.params.id, + userId: req.user.id + }); res.status(500).json({ error: "Failed to fetch rental" }); } }); @@ -355,7 +372,13 @@ router.put("/:id/status", authenticateToken, async (req, res) => { res.json(updatedRental); return; } catch (paymentError) { - console.error("Payment failed during approval:", paymentError); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Payment failed during approval", { + error: paymentError.message, + stack: paymentError.stack, + rentalId: req.params.id, + userId: req.user.id + }); // Keep rental as pending, but inform of payment failure return res.status(400).json({ error: "Payment failed during approval", @@ -538,7 +561,15 @@ router.post("/calculate-fees", authenticateToken, async (req, res) => { display: displayFees, }); } catch (error) { - console.error("Error calculating fees:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error calculating fees", { + error: error.message, + stack: error.stack, + userId: req.user.id, + startDate: req.query.startDate, + endDate: req.query.endDate, + itemId: req.query.itemId + }); res.status(500).json({ error: "Failed to calculate fees" }); } }); @@ -566,7 +597,12 @@ router.get("/earnings/status", authenticateToken, async (req, res) => { res.json(ownerRentals); } catch (error) { - console.error("Error getting earnings status:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error getting earnings status", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -580,7 +616,13 @@ router.get("/:id/refund-preview", authenticateToken, async (req, res) => { ); res.json(preview); } catch (error) { - console.error("Error getting refund preview:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error getting refund preview", { + error: error.message, + stack: error.stack, + rentalId: req.params.id, + userId: req.user.id + }); res.status(400).json({ error: error.message }); } }); @@ -618,7 +660,13 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => { refund: result.refund, }); } catch (error) { - console.error("Error cancelling rental:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error cancelling rental", { + error: error.message, + stack: error.stack, + rentalId: req.params.id, + userId: req.user.id + }); res.status(400).json({ error: error.message }); } }); diff --git a/backend/routes/stripe.js b/backend/routes/stripe.js index 03f175f..320d9a8 100644 --- a/backend/routes/stripe.js +++ b/backend/routes/stripe.js @@ -2,6 +2,7 @@ const express = require("express"); const { authenticateToken } = require("../middleware/auth"); const { User, Item } = require("../models"); const StripeService = require("../services/stripeService"); +const logger = require("../utils/logger"); const router = express.Router(); // Get checkout session status @@ -11,6 +12,14 @@ router.get("/checkout-session/:sessionId", async (req, res) => { const session = await StripeService.getCheckoutSession(sessionId); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Stripe checkout session retrieved", { + sessionId: sessionId, + status: session.status, + payment_status: session.payment_status, + metadata: session.metadata, + }); + res.json({ status: session.status, payment_status: session.payment_status, @@ -19,7 +28,12 @@ router.get("/checkout-session/:sessionId", async (req, res) => { metadata: session.metadata, }); } catch (error) { - console.error("Error retrieving checkout session:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Stripe checkout session retrieval failed", { + error: error.message, + stack: error.stack, + sessionId: sessionId, + }); res.status(500).json({ error: error.message }); } }); @@ -51,12 +65,23 @@ router.post("/accounts", authenticateToken, async (req, res) => { stripeConnectedAccountId: account.id, }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Stripe connected account created", { + userId: req.user.id, + stripeConnectedAccountId: account.id, + }); + res.json({ stripeConnectedAccountId: account.id, success: true, }); } catch (error) { - console.error("Error creating connected account:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Stripe connected account creation failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + }); res.status(500).json({ error: error.message }); } }); @@ -84,12 +109,25 @@ router.post("/account-links", authenticateToken, async (req, res) => { returnUrl ); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Stripe account link created", { + userId: req.user.id, + stripeConnectedAccountId: user.stripeConnectedAccountId, + expiresAt: accountLink.expires_at, + }); + res.json({ url: accountLink.url, expiresAt: accountLink.expires_at, }); } catch (error) { - console.error("Error creating account link:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Stripe account link creation failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + stripeConnectedAccountId: user?.stripeConnectedAccountId, + }); res.status(500).json({ error: error.message }); } }); @@ -107,6 +145,14 @@ router.get("/account-status", authenticateToken, async (req, res) => { user.stripeConnectedAccountId ); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Stripe account status retrieved", { + userId: req.user.id, + stripeConnectedAccountId: user.stripeConnectedAccountId, + detailsSubmitted: accountStatus.details_submitted, + payoutsEnabled: accountStatus.payouts_enabled, + }); + res.json({ accountId: accountStatus.id, detailsSubmitted: accountStatus.details_submitted, @@ -115,59 +161,85 @@ router.get("/account-status", authenticateToken, async (req, res) => { requirements: accountStatus.requirements, }); } catch (error) { - console.error("Error getting account status:", error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Stripe account status retrieval failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + stripeConnectedAccountId: user?.stripeConnectedAccountId, + }); res.status(500).json({ error: error.message }); } }); // Create embedded setup checkout session for collecting payment method -router.post("/create-setup-checkout-session", authenticateToken, async (req, res) => { - try { - const { rentalData } = req.body; +router.post( + "/create-setup-checkout-session", + authenticateToken, + async (req, res) => { + try { + const { rentalData } = req.body; - const user = await User.findByPk(req.user.id); - - if (!user) { - return res.status(404).json({ error: "User not found" }); - } + const user = await User.findByPk(req.user.id); - // Create or get Stripe customer - let stripeCustomerId = user.stripeCustomerId; - - if (!stripeCustomerId) { - // Create new Stripe customer - const customer = await StripeService.createCustomer({ - email: user.email, - name: `${user.firstName} ${user.lastName}`, - metadata: { - userId: user.id.toString() - } + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + // Create or get Stripe customer + let stripeCustomerId = user.stripeCustomerId; + + if (!stripeCustomerId) { + // Create new Stripe customer + const customer = await StripeService.createCustomer({ + email: user.email, + name: `${user.firstName} ${user.lastName}`, + metadata: { + userId: user.id.toString(), + }, + }); + + stripeCustomerId = customer.id; + + // Save customer ID to user record + await user.update({ stripeCustomerId }); + } + + // Add rental data to metadata if provided + const metadata = rentalData + ? { + rentalData: JSON.stringify(rentalData), + } + : {}; + + const session = await StripeService.createSetupCheckoutSession({ + customerId: stripeCustomerId, + metadata, }); - - stripeCustomerId = customer.id; - - // Save customer ID to user record - await user.update({ stripeCustomerId }); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Stripe setup checkout session created", { + userId: req.user.id, + stripeCustomerId: stripeCustomerId, + sessionId: session.id, + hasRentalData: !!rentalData, + }); + + res.json({ + clientSecret: session.client_secret, + sessionId: session.id, + }); + } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Stripe setup checkout session creation failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + stripeCustomerId: user?.stripeCustomerId, + }); + res.status(500).json({ error: error.message }); } - - // Add rental data to metadata if provided - const metadata = rentalData ? { - rentalData: JSON.stringify(rentalData) - } : {}; - - const session = await StripeService.createSetupCheckoutSession({ - customerId: stripeCustomerId, - metadata - }); - - res.json({ - clientSecret: session.client_secret, - sessionId: session.id - }); - } catch (error) { - console.error("Error creating setup checkout session:", error); - res.status(500).json({ error: error.message }); } -}); +); module.exports = router; diff --git a/backend/routes/users.js b/backend/routes/users.js index d26083e..b6cf983 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -2,6 +2,7 @@ const express = require('express'); const { User, UserAddress } = require('../models'); // Import from models/index.js to get models with associations const { authenticateToken } = require('../middleware/auth'); const { uploadProfileImage } = require('../middleware/upload'); +const logger = require('../utils/logger'); const fs = require('fs').promises; const path = require('path'); const router = express.Router(); @@ -11,8 +12,20 @@ router.get('/profile', authenticateToken, async (req, res) => { const user = await User.findByPk(req.user.id, { attributes: { exclude: ['password'] } }); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User profile fetched", { + userId: req.user.id + }); + res.json(user); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("User profile fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -24,8 +37,20 @@ router.get('/addresses', authenticateToken, async (req, res) => { where: { userId: req.user.id }, order: [['isPrimary', 'DESC'], ['createdAt', 'ASC']] }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User addresses fetched", { + userId: req.user.id, + addressCount: addresses.length + }); + res.json(addresses); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("User addresses fetch failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: error.message }); } }); @@ -36,8 +61,21 @@ router.post('/addresses', authenticateToken, async (req, res) => { ...req.body, userId: req.user.id }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User address created", { + userId: req.user.id, + addressId: address.id + }); + res.status(201).json(address); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("User address creation failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + addressData: logger.sanitize(req.body) + }); res.status(500).json({ error: error.message }); } }); @@ -55,8 +93,22 @@ router.put('/addresses/:id', authenticateToken, async (req, res) => { } await address.update(req.body); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User address updated", { + userId: req.user.id, + addressId: req.params.id + }); + res.json(address); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("User address update failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + addressId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -74,8 +126,22 @@ router.delete('/addresses/:id', authenticateToken, async (req, res) => { } await address.destroy(); + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("User address deleted", { + userId: req.user.id, + addressId: req.params.id + }); + res.status(204).send(); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("User address deletion failed", { + error: error.message, + stack: error.stack, + userId: req.user.id, + addressId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -121,13 +187,24 @@ router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { attributes: { exclude: ['password', 'email', 'phone', 'address'] } }); - + if (!user) { return res.status(404).json({ error: 'User not found' }); } - + + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Public user profile fetched", { + requestedUserId: req.params.id + }); + res.json(user); } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Public user profile fetch failed", { + error: error.message, + stack: error.stack, + requestedUserId: req.params.id + }); res.status(500).json({ error: error.message }); } }); @@ -185,7 +262,11 @@ router.put('/profile', authenticateToken, async (req, res) => { router.post('/profile/image', authenticateToken, (req, res) => { uploadProfileImage(req, res, async (err) => { if (err) { - console.error('Upload error:', err); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Profile image upload error", { + error: err.message, + userId: req.user.id + }); return res.status(400).json({ error: err.message }); } @@ -201,7 +282,12 @@ router.post('/profile/image', authenticateToken, (req, res) => { try { await fs.unlink(oldImagePath); } catch (unlinkErr) { - console.error('Error deleting old image:', unlinkErr); + const reqLogger = logger.withRequestId(req.id); + reqLogger.warn("Error deleting old profile image", { + error: unlinkErr.message, + userId: req.user.id, + oldImagePath + }); } } @@ -210,13 +296,24 @@ router.post('/profile/image', authenticateToken, (req, res) => { profileImage: req.file.filename }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Profile image uploaded successfully", { + userId: req.user.id, + filename: req.file.filename + }); + res.json({ message: 'Profile image uploaded successfully', filename: req.file.filename, imageUrl: `/uploads/profiles/${req.file.filename}` }); } catch (error) { - console.error('Database update error:', error); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Profile image database update failed", { + error: error.message, + stack: error.stack, + userId: req.user.id + }); res.status(500).json({ error: 'Failed to update profile image' }); } }); diff --git a/backend/server.js b/backend/server.js index 81fde00..618465c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -12,6 +12,8 @@ const path = require("path"); const helmet = require("helmet"); const { sequelize } = require("./models"); // Import from models/index.js to ensure associations are loaded const { cookieParser } = require("./middleware/csrf"); +const logger = require("./utils/logger"); +const morgan = require("morgan"); const authRoutes = require("./routes/auth"); const userRoutes = require("./routes/users"); @@ -34,6 +36,8 @@ const { sanitizeError, } = require("./middleware/security"); const { generalLimiter } = require("./middleware/rateLimiter"); +const errorLogger = require("./middleware/errorLogger"); +const apiLogger = require("./middleware/apiLogger"); // Apply security middleware app.use(enforceHTTPS); @@ -60,6 +64,12 @@ app.use( // Cookie parser for CSRF app.use(cookieParser); +// HTTP request logging +app.use(morgan('combined', { stream: logger.stream })); + +// API request/response logging +app.use("/api/", apiLogger); + // General rate limiting for all routes app.use("/api/", generalLimiter); @@ -107,6 +117,7 @@ app.get("/", (req, res) => { }); // Error handling middleware (must be last) +app.use(errorLogger); app.use(sanitizeError); const PORT = process.env.PORT || 5000; @@ -114,15 +125,16 @@ const PORT = process.env.PORT || 5000; sequelize .sync({ alter: true }) .then(() => { - console.log("Database synced"); + logger.info("Database synced successfully"); // Start the payout processor const payoutJobs = PayoutProcessor.startScheduledPayouts(); + logger.info("Payout processor started"); app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); + logger.info(`Server is running on port ${PORT}`, { port: PORT, environment: env }); }); }) .catch((err) => { - console.error("Unable to sync database:", err); + logger.error("Unable to sync database", { error: err.message, stack: err.stack }); }); diff --git a/backend/utils/logger.js b/backend/utils/logger.js new file mode 100644 index 0000000..0bc9f47 --- /dev/null +++ b/backend/utils/logger.js @@ -0,0 +1,137 @@ +const winston = require('winston'); +const DailyRotateFile = require('winston-daily-rotate-file'); + +// Define log levels and colors +const logLevels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +}; + +winston.addColors({ + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'white', +}); + +// Determine log level based on environment +const level = () => { + const env = process.env.NODE_ENV || 'dev'; + const isDevelopment = env === 'dev' || env === 'development'; + return isDevelopment ? 'debug' : process.env.LOG_LEVEL || 'info'; +}; + +// Define log format +const logFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), + winston.format.colorize({ all: true }), + winston.format.printf((info) => { + if (info.stack) { + return `${info.timestamp} ${info.level}: ${info.message}\n${info.stack}`; + } + return `${info.timestamp} ${info.level}: ${info.message}`; + }), +); + +// Define JSON format for file logging +const jsonFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), + winston.format.errors({ stack: true }), + winston.format.json() +); + +// Create transports array +const transports = [ + // Console transport for development + new winston.transports.Console({ + level: level(), + format: logFormat, + }), + + // Daily rotate file for all logs + new DailyRotateFile({ + filename: 'logs/application-%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxSize: '20m', + maxFiles: '14d', + level: 'info', + format: jsonFormat, + }), + + // Daily rotate file for error logs only + new DailyRotateFile({ + filename: 'logs/error-%DATE%.log', + datePattern: 'YYYY-MM-DD', + maxSize: '20m', + maxFiles: '14d', + level: 'error', + format: jsonFormat, + }), +]; + +// Create the logger +const logger = winston.createLogger({ + level: level(), + levels: logLevels, + format: jsonFormat, + transports, + exceptionHandlers: [ + new winston.transports.File({ filename: 'logs/exceptions.log' }) + ], + rejectionHandlers: [ + new winston.transports.File({ filename: 'logs/rejections.log' }) + ], +}); + +// Create a stream object for Morgan +logger.stream = { + write: (message) => { + // Remove trailing newline from Morgan and log as http level + logger.http(message.trim()); + }, +}; + +// Add request ID correlation function +logger.withRequestId = (requestId) => { + return { + error: (message, meta = {}) => logger.error(message, { ...meta, requestId }), + warn: (message, meta = {}) => logger.warn(message, { ...meta, requestId }), + info: (message, meta = {}) => logger.info(message, { ...meta, requestId }), + http: (message, meta = {}) => logger.http(message, { ...meta, requestId }), + debug: (message, meta = {}) => logger.debug(message, { ...meta, requestId }), + }; +}; + +// Add sanitization helper for sensitive data +logger.sanitize = (data) => { + if (typeof data !== 'object' || data === null) { + return data; + } + + const sensitiveFields = [ + 'password', 'token', 'secret', 'key', 'authorization', 'cookie', + 'ssn', 'credit_card', 'cvv', 'pin', 'account_number' + ]; + + const sanitized = JSON.parse(JSON.stringify(data)); + + const sanitizeObject = (obj) => { + for (const [key, value] of Object.entries(obj)) { + const lowerKey = key.toLowerCase(); + if (sensitiveFields.some(field => lowerKey.includes(field))) { + obj[key] = '[REDACTED]'; + } else if (typeof value === 'object' && value !== null) { + sanitizeObject(value); + } + } + }; + + sanitizeObject(sanitized); + return sanitized; +}; + +module.exports = logger; \ No newline at end of file