Compare commits

..

2 Commits

Author SHA1 Message Date
jackiettran
67cc997ddc Skip payment process if item is free to borrow 2025-09-22 22:02:08 -04:00
jackiettran
3e76769a3e backend logging 2025-09-22 18:38:51 -04:00
23 changed files with 1470 additions and 178 deletions

2
.gitignore vendored
View File

@@ -24,6 +24,8 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
*.log
logs/
# Editor directories and files # Editor directories and files
.idea .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 jwt = require("jsonwebtoken");
const { User } = require("../models"); // Import from models/index.js to get models with associations 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) => { const authenticateToken = async (req, res, next) => {
// First try to get token from cookie // 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({ return res.status(403).json({
error: "Invalid token", error: "Invalid token",
code: "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 // HTTPS enforcement middleware
const enforceHTTPS = (req, res, next) => { const enforceHTTPS = (req, res, next) => {
// Skip HTTPS enforcement in development // Skip HTTPS enforcement in development
@@ -20,11 +22,13 @@ const enforceHTTPS = (req, res, next) => {
// Log the redirect for monitoring // Log the redirect for monitoring
if (req.headers.host !== allowedHost) { 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, requestHost: req.headers.host,
allowedHost, allowedHost,
ip: req.ip, ip: req.ip,
url: req.url, url: req.url,
eventType: 'SECURITY_HOST_MISMATCH'
}); });
} }
@@ -70,34 +74,21 @@ const addRequestId = (req, res, next) => {
// Log security events // Log security events
const logSecurityEvent = (eventType, details, req) => { const logSecurityEvent = (eventType, details, req) => {
const reqLogger = logger.withRequestId(req.id || "unknown");
const logEntry = { const logEntry = {
timestamp: new Date().toISOString(),
eventType, eventType,
requestId: req.id || "unknown",
ip: req.ip || req.connection.remoteAddress, ip: req.ip || req.connection.remoteAddress,
userAgent: req.get("user-agent"), userAgent: req.get("user-agent"),
userId: req.user?.id || "anonymous", userId: req.user?.id || "anonymous",
...details, ...details,
}; };
// In production, this should write to a secure log file or service reqLogger.warn(`Security event: ${eventType}`, logEntry);
if (process.env.NODE_ENV === "production") {
console.log("[SECURITY]", JSON.stringify(logEntry));
} else {
console.log("[SECURITY]", eventType, details);
}
}; };
// Sanitize error messages to prevent information leakage // Sanitize error messages to prevent information leakage
const sanitizeError = (err, req, res, next) => { 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 // Send sanitized error to client
const isDevelopment = const isDevelopment =
process.env.NODE_ENV === "dev" || process.env.NODE_ENV === "development"; process.env.NODE_ENV === "dev" || process.env.NODE_ENV === "development";

View File

@@ -62,7 +62,7 @@ const Rental = sequelize.define("Rental", {
defaultValue: "pending", defaultValue: "pending",
}, },
paymentStatus: { paymentStatus: {
type: DataTypes.ENUM("pending", "paid", "refunded"), type: DataTypes.ENUM("pending", "paid", "refunded", "not_required"),
defaultValue: "pending", defaultValue: "pending",
}, },
payoutStatus: { payoutStatus: {

View File

@@ -24,13 +24,16 @@
"helmet": "^8.1.0", "helmet": "^8.1.0",
"jsdom": "^27.0.0", "jsdom": "^27.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "^1.10.1",
"multer": "^2.0.2", "multer": "^2.0.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3", "sequelize-cli": "^6.6.3",
"stripe": "^18.4.0", "stripe": "^18.4.0",
"uuid": "^11.1.0" "uuid": "^11.1.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
@@ -607,6 +610,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@csstools/color-helpers": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
@@ -739,6 +751,17 @@
"node": ">=18" "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": { "node_modules/@googlemaps/google-maps-services-js": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.4.2.tgz", "resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.4.2.tgz",
@@ -1405,6 +1428,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -1570,6 +1599,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1731,6 +1766,24 @@
"baseline-browser-mapping": "dist/cli.js" "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": { "node_modules/bcryptjs": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
@@ -2163,6 +2216,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "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", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" "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": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -2897,6 +3001,12 @@
"bser": "2.1.1" "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": { "node_modules/fetch-blob": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -2920,6 +3030,15 @@
"node": "^12.20 || >= 14.13" "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": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -2971,6 +3090,12 @@
"node": ">=8" "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": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -3615,6 +3740,12 @@
"node": ">= 0.10" "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": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -3704,7 +3835,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -4733,6 +4863,12 @@
"safe-buffer": "^5.0.1" "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": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" "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": { "node_modules/lru-cache": {
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -5002,6 +5155,49 @@
"node": "*" "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": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5285,6 +5481,15 @@
"node": ">=0.10.0" "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": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -5307,6 +5512,15 @@
"node": ">= 0.8" "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": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -5315,6 +5529,15 @@
"wrappy": "1" "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": { "node_modules/onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "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": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -6249,6 +6481,15 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/simple-update-notifier": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -6366,6 +6607,15 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "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": { "node_modules/stack-utils": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -6732,6 +6982,12 @@
"node": "*" "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": { "node_modules/tldts": {
"version": "7.0.14", "version": "7.0.14",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz",
@@ -6815,6 +7071,15 @@
"node": ">=18" "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": { "node_modules/tsscmp": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
@@ -7120,6 +7385,60 @@
"node": ">= 8" "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": { "node_modules/wkx": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",

View File

@@ -36,13 +36,16 @@
"helmet": "^8.1.0", "helmet": "^8.1.0",
"jsdom": "^27.0.0", "jsdom": "^27.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "^1.10.1",
"multer": "^2.0.2", "multer": "^2.0.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3", "sequelize-cli": "^6.6.3",
"stripe": "^18.4.0", "stripe": "^18.4.0",
"uuid": "^11.1.0" "uuid": "^11.1.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",

View File

@@ -2,6 +2,7 @@ const express = require("express");
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
const { OAuth2Client } = require("google-auth-library"); const { OAuth2Client } = require("google-auth-library");
const { User } = require("../models"); // Import from models/index.js to get models with associations const { User } = require("../models"); // Import from models/index.js to get models with associations
const logger = require("../utils/logger");
const { const {
sanitizeInput, sanitizeInput,
validateRegistration, validateRegistration,
@@ -84,6 +85,13 @@ router.post(
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days 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({ res.status(201).json({
user: { user: {
id: user.id, id: user.id,
@@ -95,7 +103,13 @@ router.post(
// Don't send token in response body for security // Don't send token in response body for security
}); });
} catch (error) { } 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." }); res.status(500).json({ error: "Registration failed. Please try again." });
} }
} }
@@ -164,6 +178,12 @@ router.post(
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days 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({ res.json({
user: { user: {
id: user.id, id: user.id,
@@ -175,7 +195,12 @@ router.post(
// Don't send token in response body for security // Don't send token in response body for security
}); });
} catch (error) { } 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." }); res.status(500).json({ error: "Login failed. Please try again." });
} }
} }
@@ -271,6 +296,13 @@ router.post(
maxAge: 7 * 24 * 60 * 60 * 1000, 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({ res.json({
user: { user: {
id: user.id, id: user.id,
@@ -298,7 +330,12 @@ router.post(
.status(400) .status(400)
.json({ error: "Malformed Google token. Please try again." }); .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 res
.status(500) .status(500)
.json({ error: "Google authentication failed. Please try again." }); .json({ error: "Google authentication failed. Please try again." });
@@ -341,6 +378,11 @@ router.post("/refresh", async (req, res) => {
maxAge: 15 * 60 * 1000, maxAge: 15 * 60 * 1000,
}); });
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Token refresh successful", {
userId: user.id
});
res.json({ res.json({
user: { user: {
id: user.id, id: user.id,
@@ -351,13 +393,23 @@ router.post("/refresh", async (req, res) => {
}, },
}); });
} catch (error) { } 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" }); res.status(401).json({ error: "Invalid or expired refresh token" });
} }
}); });
// Logout endpoint // Logout endpoint
router.post("/logout", (req, res) => { router.post("/logout", (req, res) => {
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User logout", {
userId: req.user?.id || 'anonymous'
});
// Clear cookies // Clear cookies
res.clearCookie("accessToken"); res.clearCookie("accessToken");
res.clearCookie("refreshToken"); res.clearCookie("refreshToken");

View File

@@ -2,6 +2,7 @@ const express = require('express');
const { Op } = require('sequelize'); const { Op } = require('sequelize');
const { ItemRequest, ItemRequestResponse, User, Item } = require('../models'); const { ItemRequest, ItemRequestResponse, User, Item } = require('../models');
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
@@ -38,6 +39,15 @@ router.get('/', async (req, res) => {
order: [['createdAt', 'DESC']] 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({ res.json({
requests: rows, requests: rows,
totalPages: Math.ceil(count / limit), totalPages: Math.ceil(count / limit),
@@ -45,6 +55,12 @@ router.get('/', async (req, res) => {
totalRequests: count totalRequests: count
}); });
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -78,8 +94,20 @@ router.get('/my-requests', authenticateToken, async (req, res) => {
order: [['createdAt', 'DESC']] 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); res.json(requests);
} catch (error) { } 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 }); 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' }); 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); res.json(request);
} catch (error) { } 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 }); 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); res.status(201).json(requestWithRequester);
} catch (error) { } 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 }); 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); res.json(updatedRequest);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -187,8 +254,22 @@ router.delete('/:id', authenticateToken, async (req, res) => {
} }
await request.destroy(); 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(); res.status(204).send();
} catch (error) { } 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 }); 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); res.status(201).json(responseWithDetails);
} catch (error) { } 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 }); 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); res.json(updatedResponse);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });

View File

@@ -2,6 +2,7 @@ const express = require("express");
const { Op } = require("sequelize"); const { Op } = require("sequelize");
const { Item, User, Rental } = require("../models"); // Import from models/index.js to get models with associations const { Item, User, Rental } = require("../models"); // Import from models/index.js to get models with associations
const { authenticateToken } = require("../middleware/auth"); const { authenticateToken } = require("../middleware/auth");
const logger = require("../utils/logger");
const router = express.Router(); const router = express.Router();
router.get("/", async (req, res) => { router.get("/", async (req, res) => {
@@ -60,6 +61,14 @@ router.get("/", async (req, res) => {
return itemData; 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({ res.json({
items: itemsWithRoundedCoords, items: itemsWithRoundedCoords,
totalPages: Math.ceil(count / limit), totalPages: Math.ceil(count / limit),
@@ -67,6 +76,12 @@ router.get("/", async (req, res) => {
totalItems: count, totalItems: count,
}); });
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -87,8 +102,20 @@ router.get("/recommendations", authenticateToken, async (req, res) => {
order: [["createdAt", "DESC"]], order: [["createdAt", "DESC"]],
}); });
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Recommendations fetched", {
userId: req.user.id,
recommendationsCount: recommendations.length
});
res.json(recommendations); res.json(recommendations);
} catch (error) { } 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 }); 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 ? reviews.reduce((sum, review) => sum + review.itemRating, 0) / reviews.length
: 0; : 0;
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Item reviews fetched", {
itemId: req.params.id,
reviewsCount: reviews.length,
averageRating
});
res.json({ res.json({
reviews, reviews,
averageRating, averageRating,
totalReviews: reviews.length totalReviews: reviews.length
}); });
} catch (error) { } 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 }); 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; 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); res.json(itemResponse);
} catch (error) { } 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 }); 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); res.status(201).json(itemWithOwner);
} catch (error) { } 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 }); 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); res.json(updatedItem);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -227,8 +306,22 @@ router.delete("/:id", authenticateToken, async (req, res) => {
} }
await item.destroy(); 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(); res.status(204).send();
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });

View File

@@ -3,6 +3,7 @@ const router = express.Router();
const { authenticateToken } = require("../middleware/auth"); const { authenticateToken } = require("../middleware/auth");
const rateLimiter = require("../middleware/rateLimiter"); const rateLimiter = require("../middleware/rateLimiter");
const googleMapsService = require("../services/googleMapsService"); const googleMapsService = require("../services/googleMapsService");
const logger = require("../utils/logger");
// Input validation middleware // Input validation middleware
const validateInput = (req, res, next) => { const validateInput = (req, res, next) => {
@@ -34,8 +35,12 @@ const validateInput = (req, res, next) => {
}; };
// Error handling middleware // Error handling middleware
const handleServiceError = (error, res) => { const handleServiceError = (error, res, req) => {
console.error("Maps service error:", error.message); 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")) { if (error.message.includes("API key not configured")) {
return res.status(503).json({ return res.status(503).json({
@@ -87,17 +92,16 @@ router.post(
); );
// Log request for monitoring (without sensitive data) // Log request for monitoring (without sensitive data)
console.log( const reqLogger = logger.withRequestId(req.id);
`Places Autocomplete: user=${ reqLogger.info("Places Autocomplete request", {
req.user?.id || "anonymous" userId: req.user?.id || "anonymous",
}, query_length=${input.length}, results=${ queryLength: input.length,
result.predictions?.length || 0 resultsCount: result.predictions?.length || 0
}` });
);
res.json(result); res.json(result);
} catch (error) { } catch (error) {
handleServiceError(error, res); handleServiceError(error, res, req);
} }
} }
); );
@@ -127,15 +131,15 @@ router.post(
const result = await googleMapsService.getPlaceDetails(placeId, options); const result = await googleMapsService.getPlaceDetails(placeId, options);
// Log request for monitoring // Log request for monitoring
console.log( const reqLogger = logger.withRequestId(req.id);
`Place Details: user=${ reqLogger.info("Place Details request", {
req.user?.id || "anonymous" userId: req.user?.id || "anonymous",
}, placeId=${placeId.substring(0, 10)}...` placeIdPrefix: placeId.substring(0, 10) + "..."
); });
res.json(result); res.json(result);
} catch (error) { } catch (error) {
handleServiceError(error, res); handleServiceError(error, res, req);
} }
} }
); );
@@ -165,15 +169,15 @@ router.post(
const result = await googleMapsService.geocodeAddress(address, options); const result = await googleMapsService.geocodeAddress(address, options);
// Log request for monitoring // Log request for monitoring
console.log( const reqLogger = logger.withRequestId(req.id);
`Geocoding: user=${req.user?.id || "anonymous"}, address_length=${ reqLogger.info("Geocoding request", {
address.length userId: req.user?.id || "anonymous",
}` addressLength: address.length
); });
res.json(result); res.json(result);
} catch (error) { } catch (error) {
handleServiceError(error, res); handleServiceError(error, res, req);
} }
} }
); );

View File

@@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const { Message, User } = require('../models'); const { Message, User } = require('../models');
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const logger = require('../utils/logger');
const router = express.Router(); const router = express.Router();
// Get all messages for the current user (inbox) // Get all messages for the current user (inbox)
@@ -17,8 +18,21 @@ router.get('/', authenticateToken, async (req, res) => {
], ],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Messages inbox fetched", {
userId: req.user.id,
messageCount: messages.length
});
res.json(messages); res.json(messages);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -37,8 +51,21 @@ router.get('/sent', authenticateToken, async (req, res) => {
], ],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Sent messages fetched", {
userId: req.user.id,
messageCount: messages.length
});
res.json(messages); res.json(messages);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -86,8 +113,22 @@ router.get('/:id', authenticateToken, async (req, res) => {
await message.update({ isRead: true }); 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); res.json(message);
} catch (error) { } 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 }); 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); res.status(201).json(messageWithSender);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -145,8 +201,22 @@ router.put('/:id/read', authenticateToken, async (req, res) => {
} }
await message.update({ isRead: true }); 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); res.json(message);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -160,8 +230,20 @@ router.get('/unread/count', authenticateToken, async (req, res) => {
isRead: false isRead: false
} }
}); });
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Unread message count fetched", {
userId: req.user.id,
unreadCount: count
});
res.json({ count }); res.json({ count });
} catch (error) { } 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 }); 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 { authenticateToken } = require("../middleware/auth");
const FeeCalculator = require("../utils/feeCalculator"); const FeeCalculator = require("../utils/feeCalculator");
const RefundService = require("../services/refundService"); const RefundService = require("../services/refundService");
const logger = require("../utils/logger");
const router = express.Router(); const router = express.Router();
// Helper function to check and update review visibility // Helper function to check and update review visibility
@@ -67,7 +68,12 @@ router.get("/my-rentals", authenticateToken, async (req, res) => {
res.json(rentals); res.json(rentals);
} catch (error) { } 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" }); res.status(500).json({ error: "Failed to fetch rentals" });
} }
}); });
@@ -90,7 +96,12 @@ router.get("/my-listings", authenticateToken, async (req, res) => {
res.json(rentals); res.json(rentals);
} catch (error) { } 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" }); res.status(500).json({ error: "Failed to fetch listings" });
} }
}); });
@@ -125,7 +136,13 @@ router.get("/:id", authenticateToken, async (req, res) => {
res.json(rental); res.json(rental);
} catch (error) { } 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" }); res.status(500).json({ error: "Failed to fetch rental" });
} }
}); });
@@ -216,12 +233,12 @@ router.post("/", authenticateToken, async (req, res) => {
// Calculate fees using FeeCalculator // Calculate fees using FeeCalculator
const fees = FeeCalculator.calculateRentalFees(totalAmount); const fees = FeeCalculator.calculateRentalFees(totalAmount);
// Validate that payment method was provided // Validate that payment method was provided for paid rentals
if (!stripePaymentMethodId) { if (totalAmount > 0 && !stripePaymentMethodId) {
return res.status(400).json({ error: "Payment method is required" }); return res.status(400).json({ error: "Payment method is required for paid rentals" });
} }
const rental = await Rental.create({ const rentalData = {
itemId, itemId,
renterId: req.user.id, renterId: req.user.id,
ownerId: item.ownerId, ownerId: item.ownerId,
@@ -230,13 +247,19 @@ router.post("/", authenticateToken, async (req, res) => {
totalAmount: fees.totalChargedAmount, totalAmount: fees.totalChargedAmount,
platformFee: fees.platformFee, platformFee: fees.platformFee,
payoutAmount: fees.payoutAmount, payoutAmount: fees.payoutAmount,
paymentStatus: "pending", paymentStatus: totalAmount > 0 ? "pending" : "not_required",
status: "pending", status: "pending",
deliveryMethod, deliveryMethod,
deliveryAddress, deliveryAddress,
notes, notes,
stripePaymentMethodId, };
});
// Only add stripePaymentMethodId if it's provided (for paid rentals)
if (stripePaymentMethodId) {
rentalData.stripePaymentMethodId = stripePaymentMethodId;
}
const rental = await Rental.create(rentalData);
const rentalWithDetails = await Rental.findByPk(rental.id, { const rentalWithDetails = await Rental.findByPk(rental.id, {
include: [ include: [
@@ -293,17 +316,19 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
return res.status(403).json({ error: "Unauthorized to update this rental" }); return res.status(403).json({ error: "Unauthorized to update this rental" });
} }
// If owner is approving a pending rental, charge the stored payment method // If owner is approving a pending rental, handle payment for paid rentals
if ( if (
status === "confirmed" && status === "confirmed" &&
rental.status === "pending" && rental.status === "pending" &&
rental.ownerId === req.user.id rental.ownerId === req.user.id
) { ) {
if (!rental.stripePaymentMethodId) { // Skip payment processing for free rentals
return res if (rental.totalAmount > 0) {
.status(400) if (!rental.stripePaymentMethodId) {
.json({ error: "No payment method found for this rental" }); return res
} .status(400)
.json({ error: "No payment method found for this rental" });
}
try { try {
// Import StripeService to process the payment // Import StripeService to process the payment
@@ -355,13 +380,44 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
res.json(updatedRental); res.json(updatedRental);
return; return;
} catch (paymentError) { } 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 // Keep rental as pending, but inform of payment failure
return res.status(400).json({ return res.status(400).json({
error: "Payment failed during approval", error: "Payment failed during approval",
details: paymentError.message, details: paymentError.message,
}); });
} }
} else {
// For free rentals, just update status directly
await rental.update({
status: "confirmed"
});
const updatedRental = await Rental.findByPk(rental.id, {
include: [
{ model: Item, as: "item" },
{
model: User,
as: "owner",
attributes: ["id", "username", "firstName", "lastName"],
},
{
model: User,
as: "renter",
attributes: ["id", "username", "firstName", "lastName"],
},
],
});
res.json(updatedRental);
return;
}
} }
await rental.update({ status }); await rental.update({ status });
@@ -538,7 +594,15 @@ router.post("/calculate-fees", authenticateToken, async (req, res) => {
display: displayFees, display: displayFees,
}); });
} catch (error) { } 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" }); res.status(500).json({ error: "Failed to calculate fees" });
} }
}); });
@@ -566,7 +630,12 @@ router.get("/earnings/status", authenticateToken, async (req, res) => {
res.json(ownerRentals); res.json(ownerRentals);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -580,7 +649,13 @@ router.get("/:id/refund-preview", authenticateToken, async (req, res) => {
); );
res.json(preview); res.json(preview);
} catch (error) { } 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 }); res.status(400).json({ error: error.message });
} }
}); });
@@ -618,7 +693,13 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
refund: result.refund, refund: result.refund,
}); });
} catch (error) { } 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 }); res.status(400).json({ error: error.message });
} }
}); });

View File

@@ -2,6 +2,7 @@ const express = require("express");
const { authenticateToken } = require("../middleware/auth"); const { authenticateToken } = require("../middleware/auth");
const { User, Item } = require("../models"); const { User, Item } = require("../models");
const StripeService = require("../services/stripeService"); const StripeService = require("../services/stripeService");
const logger = require("../utils/logger");
const router = express.Router(); const router = express.Router();
// Get checkout session status // Get checkout session status
@@ -11,6 +12,14 @@ router.get("/checkout-session/:sessionId", async (req, res) => {
const session = await StripeService.getCheckoutSession(sessionId); 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({ res.json({
status: session.status, status: session.status,
payment_status: session.payment_status, payment_status: session.payment_status,
@@ -19,7 +28,12 @@ router.get("/checkout-session/:sessionId", async (req, res) => {
metadata: session.metadata, metadata: session.metadata,
}); });
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -51,12 +65,23 @@ router.post("/accounts", authenticateToken, async (req, res) => {
stripeConnectedAccountId: account.id, stripeConnectedAccountId: account.id,
}); });
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Stripe connected account created", {
userId: req.user.id,
stripeConnectedAccountId: account.id,
});
res.json({ res.json({
stripeConnectedAccountId: account.id, stripeConnectedAccountId: account.id,
success: true, success: true,
}); });
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -84,12 +109,25 @@ router.post("/account-links", authenticateToken, async (req, res) => {
returnUrl 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({ res.json({
url: accountLink.url, url: accountLink.url,
expiresAt: accountLink.expires_at, expiresAt: accountLink.expires_at,
}); });
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -107,6 +145,14 @@ router.get("/account-status", authenticateToken, async (req, res) => {
user.stripeConnectedAccountId 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({ res.json({
accountId: accountStatus.id, accountId: accountStatus.id,
detailsSubmitted: accountStatus.details_submitted, detailsSubmitted: accountStatus.details_submitted,
@@ -115,59 +161,85 @@ router.get("/account-status", authenticateToken, async (req, res) => {
requirements: accountStatus.requirements, requirements: accountStatus.requirements,
}); });
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
// Create embedded setup checkout session for collecting payment method // Create embedded setup checkout session for collecting payment method
router.post("/create-setup-checkout-session", authenticateToken, async (req, res) => { router.post(
try { "/create-setup-checkout-session",
const { rentalData } = req.body; 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" });
}
// Create or get Stripe customer if (!user) {
let stripeCustomerId = user.stripeCustomerId; return res.status(404).json({ error: "User not found" });
}
if (!stripeCustomerId) {
// Create new Stripe customer // Create or get Stripe customer
const customer = await StripeService.createCustomer({ let stripeCustomerId = user.stripeCustomerId;
email: user.email,
name: `${user.firstName} ${user.lastName}`, if (!stripeCustomerId) {
metadata: { // Create new Stripe customer
userId: user.id.toString() 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", {
// Save customer ID to user record userId: req.user.id,
await user.update({ stripeCustomerId }); 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; 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 { User, UserAddress } = require('../models'); // Import from models/index.js to get models with associations
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const { uploadProfileImage } = require('../middleware/upload'); const { uploadProfileImage } = require('../middleware/upload');
const logger = require('../utils/logger');
const fs = require('fs').promises; const fs = require('fs').promises;
const path = require('path'); const path = require('path');
const router = express.Router(); const router = express.Router();
@@ -11,8 +12,20 @@ router.get('/profile', authenticateToken, async (req, res) => {
const user = await User.findByPk(req.user.id, { const user = await User.findByPk(req.user.id, {
attributes: { exclude: ['password'] } attributes: { exclude: ['password'] }
}); });
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User profile fetched", {
userId: req.user.id
});
res.json(user); res.json(user);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -24,8 +37,20 @@ router.get('/addresses', authenticateToken, async (req, res) => {
where: { userId: req.user.id }, where: { userId: req.user.id },
order: [['isPrimary', 'DESC'], ['createdAt', 'ASC']] 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); res.json(addresses);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -36,8 +61,21 @@ router.post('/addresses', authenticateToken, async (req, res) => {
...req.body, ...req.body,
userId: req.user.id 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); res.status(201).json(address);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -55,8 +93,22 @@ router.put('/addresses/:id', authenticateToken, async (req, res) => {
} }
await address.update(req.body); 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); res.json(address);
} catch (error) { } 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 }); res.status(500).json({ error: error.message });
} }
}); });
@@ -74,8 +126,22 @@ router.delete('/addresses/:id', authenticateToken, async (req, res) => {
} }
await address.destroy(); 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(); res.status(204).send();
} catch (error) { } 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 }); 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, { const user = await User.findByPk(req.params.id, {
attributes: { exclude: ['password', 'email', 'phone', 'address'] } attributes: { exclude: ['password', 'email', 'phone', 'address'] }
}); });
if (!user) { if (!user) {
return res.status(404).json({ error: 'User not found' }); 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); res.json(user);
} catch (error) { } 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 }); 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) => { router.post('/profile/image', authenticateToken, (req, res) => {
uploadProfileImage(req, res, async (err) => { uploadProfileImage(req, res, async (err) => {
if (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 }); return res.status(400).json({ error: err.message });
} }
@@ -201,7 +282,12 @@ router.post('/profile/image', authenticateToken, (req, res) => {
try { try {
await fs.unlink(oldImagePath); await fs.unlink(oldImagePath);
} catch (unlinkErr) { } 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 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({ res.json({
message: 'Profile image uploaded successfully', message: 'Profile image uploaded successfully',
filename: req.file.filename, filename: req.file.filename,
imageUrl: `/uploads/profiles/${req.file.filename}` imageUrl: `/uploads/profiles/${req.file.filename}`
}); });
} catch (error) { } 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' }); 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 helmet = require("helmet");
const { sequelize } = require("./models"); // Import from models/index.js to ensure associations are loaded const { sequelize } = require("./models"); // Import from models/index.js to ensure associations are loaded
const { cookieParser } = require("./middleware/csrf"); const { cookieParser } = require("./middleware/csrf");
const logger = require("./utils/logger");
const morgan = require("morgan");
const authRoutes = require("./routes/auth"); const authRoutes = require("./routes/auth");
const userRoutes = require("./routes/users"); const userRoutes = require("./routes/users");
@@ -34,6 +36,8 @@ const {
sanitizeError, sanitizeError,
} = require("./middleware/security"); } = require("./middleware/security");
const { generalLimiter } = require("./middleware/rateLimiter"); const { generalLimiter } = require("./middleware/rateLimiter");
const errorLogger = require("./middleware/errorLogger");
const apiLogger = require("./middleware/apiLogger");
// Apply security middleware // Apply security middleware
app.use(enforceHTTPS); app.use(enforceHTTPS);
@@ -60,6 +64,12 @@ app.use(
// Cookie parser for CSRF // Cookie parser for CSRF
app.use(cookieParser); 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 // General rate limiting for all routes
app.use("/api/", generalLimiter); app.use("/api/", generalLimiter);
@@ -107,6 +117,7 @@ app.get("/", (req, res) => {
}); });
// Error handling middleware (must be last) // Error handling middleware (must be last)
app.use(errorLogger);
app.use(sanitizeError); app.use(sanitizeError);
const PORT = process.env.PORT || 5000; const PORT = process.env.PORT || 5000;
@@ -114,15 +125,16 @@ const PORT = process.env.PORT || 5000;
sequelize sequelize
.sync({ alter: true }) .sync({ alter: true })
.then(() => { .then(() => {
console.log("Database synced"); logger.info("Database synced successfully");
// Start the payout processor // Start the payout processor
const payoutJobs = PayoutProcessor.startScheduledPayouts(); const payoutJobs = PayoutProcessor.startScheduledPayouts();
logger.info("Payout processor started");
app.listen(PORT, () => { 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) => { .catch((err) => {
console.error("Unable to sync database:", err); logger.error("Unable to sync database", { error: err.message, stack: err.stack });
}); });

View File

@@ -92,8 +92,8 @@ class RefundService {
}; };
} }
// Check payment status // Check payment status - allow cancellation for both paid and free rentals
if (rental.paymentStatus !== "paid") { if (rental.paymentStatus !== "paid" && rental.paymentStatus !== "not_required") {
return { return {
canCancel: false, canCancel: false,
reason: "Cannot cancel rental that hasn't been paid", reason: "Cannot cancel rental that hasn't been paid",

View File

@@ -323,7 +323,7 @@ describe('Rentals Routes', () => {
expect(response.body).toEqual({ error: 'Item is already booked for these dates' }); expect(response.body).toEqual({ error: 'Item is already booked for these dates' });
}); });
it('should return 400 when payment method is missing', async () => { it('should return 400 when payment method is missing for paid rentals', async () => {
const dataWithoutPayment = { ...rentalData }; const dataWithoutPayment = { ...rentalData };
delete dataWithoutPayment.stripePaymentMethodId; delete dataWithoutPayment.stripePaymentMethodId;
@@ -332,7 +332,49 @@ describe('Rentals Routes', () => {
.send(dataWithoutPayment); .send(dataWithoutPayment);
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Payment method is required' }); expect(response.body).toEqual({ error: 'Payment method is required for paid rentals' });
});
it('should create a free rental without payment method', async () => {
// Set up a free item (both prices are 0)
Item.findByPk.mockResolvedValue({
id: 1,
ownerId: 2,
availability: true,
pricePerHour: 0,
pricePerDay: 0
});
const freeRentalData = { ...rentalData };
delete freeRentalData.stripePaymentMethodId;
const createdRental = {
id: 1,
...freeRentalData,
renterId: 1,
ownerId: 2,
totalAmount: 0,
platformFee: 0,
payoutAmount: 0,
paymentStatus: 'not_required',
status: 'pending'
};
Rental.create.mockResolvedValue(createdRental);
Rental.findByPk.mockResolvedValue({
...createdRental,
item: { id: 1, name: 'Free Item' },
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' },
renter: { id: 1, username: 'renter1', firstName: 'Jane', lastName: 'Smith' }
});
const response = await request(app)
.post('/rentals')
.send(freeRentalData);
expect(response.status).toBe(201);
expect(response.body.paymentStatus).toBe('not_required');
expect(response.body.totalAmount).toBe(0);
}); });
it('should handle database errors during creation', async () => { it('should handle database errors during creation', async () => {
@@ -422,6 +464,34 @@ describe('Rentals Routes', () => {
}); });
}); });
it('should approve free rental without payment processing', async () => {
const freeRental = {
...mockRental,
totalAmount: 0,
paymentStatus: 'not_required',
stripePaymentMethodId: null,
update: jest.fn().mockResolvedValue(true)
};
mockRentalFindByPk.mockResolvedValueOnce(freeRental);
const updatedFreeRental = {
...freeRental,
status: 'confirmed'
};
mockRentalFindByPk.mockResolvedValueOnce(updatedFreeRental);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(200);
expect(StripeService.chargePaymentMethod).not.toHaveBeenCalled();
expect(freeRental.update).toHaveBeenCalledWith({
status: 'confirmed'
});
});
it('should return 400 when renter has no Stripe customer ID', async () => { it('should return 400 when renter has no Stripe customer ID', async () => {
const rentalWithoutStripeCustomer = { const rentalWithoutStripeCustomer = {
...mockRental, ...mockRental,

View File

@@ -295,6 +295,17 @@ describe('RefundService', () => {
cancelledBy: null cancelledBy: null
}); });
}); });
it('should allow cancellation for free rental with not_required payment status', () => {
const rental = { ...baseRental, paymentStatus: 'not_required' };
const result = RefundService.validateCancellationEligibility(rental, 100);
expect(result).toEqual({
canCancel: true,
reason: 'Cancellation allowed',
cancelledBy: 'renter'
});
});
}); });
describe('Edge cases', () => { describe('Edge cases', () => {

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;

View File

@@ -152,7 +152,10 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title"> <h5 className="modal-title">
{success ? "Refund Confirmation" : "Cancel Rental"} {success
? (rental.totalAmount > 0 ? "Refund Confirmation" : "Cancellation Confirmation")
: "Cancel Rental"
}
</h5> </h5>
<button <button
type="button" type="button"
@@ -167,22 +170,26 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
<div className="mb-4"> <div className="mb-4">
<i className="bi bi-check-circle-fill text-success" style={{ fontSize: '4rem' }}></i> <i className="bi bi-check-circle-fill text-success" style={{ fontSize: '4rem' }}></i>
</div> </div>
<h3 className="text-success mb-3">Refund Processed Successfully!</h3> <h3 className="text-success mb-3">
<div className="alert alert-success mb-4"> {rental.totalAmount > 0 ? 'Refund Processed Successfully!' : 'Rental Cancelled Successfully!'}
<h5 className="mb-3"> </h3>
<strong>{formatCurrency(processedRefund.amount)}</strong> has been refunded {rental.totalAmount > 0 && (
</h5> <div className="alert alert-success mb-4">
<div className="small text-muted"> <h5 className="mb-3">
<p className="mb-2"> <strong>{formatCurrency(processedRefund.amount)}</strong> has been refunded
<i className="bi bi-clock me-2"></i> </h5>
Your refund will appear in your payment method within <strong>3-5 business days</strong> <div className="small text-muted">
</p> <p className="mb-2">
<p className="mb-0"> <i className="bi bi-clock me-2"></i>
<i className="bi bi-credit-card me-2"></i> Your refund will appear in your payment method within <strong>3-5 business days</strong>
Refund will be processed to your original payment method </p>
</p> <p className="mb-0">
<i className="bi bi-credit-card me-2"></i>
Refund will be processed to your original payment method
</p>
</div>
</div> </div>
</div> )}
<p className="text-muted mb-4"> <p className="text-muted mb-4">
Thank you for using our platform. We hope you'll rent with us again soon! Thank you for using our platform. We hope you'll rent with us again soon!
</p> </p>
@@ -227,28 +234,30 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
</div> </div>
</div> </div>
<div className="mb-4"> {rental.totalAmount > 0 && (
<h5>Refund Information</h5> <div className="mb-4">
<div <h5>Refund Information</h5>
className={`alert alert-${getRefundColor( <div
refundPreview.refundPercentage className={`alert alert-${getRefundColor(
)}`} refundPreview.refundPercentage
> )}`}
<div className="d-flex justify-content-between align-items-center"> >
<div> <div className="d-flex justify-content-between align-items-center">
<strong>Refund Amount:</strong>{" "} <div>
{formatCurrency(refundPreview.refundAmount)} <strong>Refund Amount:</strong>{" "}
</div> {formatCurrency(refundPreview.refundAmount)}
<div> </div>
<strong> <div>
{Math.round(refundPreview.refundPercentage * 100)}% <strong>
</strong> {Math.round(refundPreview.refundPercentage * 100)}%
</strong>
</div>
</div> </div>
<hr />
<small>{refundPreview.reason}</small>
</div> </div>
<hr />
<small>{refundPreview.reason}</small>
</div> </div>
</div> )}
<form> <form>
<div className="mb-3"> <div className="mb-3">
@@ -310,11 +319,13 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
Processing... Processing...
</> </>
) : ( ) : (
`Cancel with ${ rental.totalAmount > 0
refundPreview.refundAmount > 0 ? `Cancel with ${
? `Refund ${formatCurrency(refundPreview.refundAmount)}` refundPreview.refundAmount > 0
: "No Refund" ? `Refund ${formatCurrency(refundPreview.refundAmount)}`
}` : "No Refund"
}`
: "Cancel Rental"
)} )}
</button> </button>
)} )}

View File

@@ -143,6 +143,21 @@ const RentItem: React.FC = () => {
} }
}; };
const handleFreeBorrow = async () => {
const rentalData = getRentalData();
if (!rentalData) return;
try {
setError(null);
await rentalAPI.createRental(rentalData);
setCompleted(true);
} catch (error: any) {
setError(
error.response?.data?.error || "Failed to create rental request"
);
}
};
const handleChange = ( const handleChange = (
e: React.ChangeEvent< e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
@@ -202,7 +217,7 @@ const RentItem: React.FC = () => {
<i className="bi bi-check-circle-fill display-1 text-success mb-3"></i> <i className="bi bi-check-circle-fill display-1 text-success mb-3"></i>
<h3>Rental Request Sent!</h3> <h3>Rental Request Sent!</h3>
<p className="mb-3"> <p className="mb-3">
Your rental request has been submitted to the owner. Your rental request has been submitted to the owner.
You'll only be charged if they approve your request. You'll only be charged if they approve your request.
</p> </p>
<div className="d-grid gap-2 d-md-block"> <div className="d-grid gap-2 d-md-block">
@@ -225,17 +240,54 @@ const RentItem: React.FC = () => {
) : ( ) : (
<div className="card mb-4"> <div className="card mb-4">
<div className="card-body"> <div className="card-body">
<h5 className="card-title">Complete Your Rental Request</h5> <h5 className="card-title">
<p className="text-muted small mb-3"> {totalCost === 0
Add your payment method to complete your rental request. ? "Complete Your Borrow Request"
You'll only be charged if the owner approves your request. : "Complete Your Rental Request"}
</p> </h5>
{totalCost > 0 && (
<p className="text-muted small mb-3">
Add your payment method to complete your rental request.
You'll only be charged if the owner approves your
request.
</p>
)}
{!manualSelection.startDate || !manualSelection.endDate || !getRentalData() ? ( {!manualSelection.startDate ||
!manualSelection.endDate ||
!getRentalData() ? (
<div className="alert alert-info"> <div className="alert alert-info">
<i className="bi bi-info-circle me-2"></i> <i className="bi bi-info-circle me-2"></i>
Please complete the rental dates and details above to proceed with payment setup. Please complete the rental dates and details above to
proceed with{" "}
{totalCost === 0
? "your borrow request"
: "payment setup"}
.
</div> </div>
) : totalCost === 0 ? (
<>
<div className="alert alert-success">
<i className="bi bi-check-circle me-2"></i>
This item is free to borrow! No payment required
</div>
<div className="d-grid gap-2">
<button
type="button"
className="btn btn-primary"
onClick={handleFreeBorrow}
>
Confirm Borrow Request
</button>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => navigate(`/items/${id}`)}
>
Cancel Request
</button>
</div>
</>
) : ( ) : (
<> <>
<EmbeddedStripeCheckout <EmbeddedStripeCheckout
@@ -243,7 +295,7 @@ const RentItem: React.FC = () => {
onSuccess={() => setCompleted(true)} onSuccess={() => setCompleted(true)}
onError={(error) => setError(error)} onError={(error) => setError(error)}
/> />
<div className="text-center mt-3"> <div className="text-center mt-3">
<button <button
type="button" type="button"
@@ -280,7 +332,7 @@ const RentItem: React.FC = () => {
<p className="text-muted small"> <p className="text-muted small">
{item.city && item.state {item.city && item.state
? `${item.city}, ${item.state}` ? `${item.city}, ${item.state}`
: ''} : ""}
</p> </p>
<hr /> <hr />