backend logging

This commit is contained in:
jackiettran
2025-09-22 18:38:51 -04:00
parent 6199609a4d
commit 3e76769a3e
17 changed files with 1225 additions and 110 deletions

2
.gitignore vendored
View File

@@ -24,6 +24,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
*.log
logs/
# Editor directories and files
.idea

View File

@@ -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;

View File

@@ -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",

View File

@@ -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;

View File

@@ -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";

View File

@@ -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",

View File

@@ -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",

View File

@@ -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");

View File

@@ -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 });
}
});

View File

@@ -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 });
}
});

View File

@@ -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);
}
}
);

View File

@@ -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 });
}
});

View File

@@ -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 });
}
});

View File

@@ -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);
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Create or get Stripe customer
let stripeCustomerId = user.stripeCustomerId;
// 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 (!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;
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe setup checkout session created", {
userId: req.user.id,
stripeCustomerId: stripeCustomerId,
sessionId: session.id,
hasRentalData: !!rentalData,
});
// Save customer ID to user record
await user.update({ stripeCustomerId });
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;

View File

@@ -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 });
}
});
@@ -126,8 +192,19 @@ router.get('/:id', async (req, res) => {
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' });
}
});

View File

@@ -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 });
});

137
backend/utils/logger.js Normal file
View File

@@ -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;