From 76e4039ba89342b55e9ff345d995319fcf9c119f Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Thu, 25 Dec 2025 18:41:42 -0500 Subject: [PATCH] added stack trace to some logging --- backend/middleware/apiLogger.js | 28 ++ backend/middleware/rateLimiter.js | 22 ++ backend/routes/auth.js | 4 + backend/routes/conditionChecks.js | 4 + backend/routes/feedback.js | 2 + backend/routes/forum.js | 16 +- backend/routes/items.js | 20 +- backend/routes/messages.js | 2 + backend/routes/rentals.js | 8 + backend/services/UserService.js | 4 + .../email/domain/RentalFlowEmailService.js | 27 +- backend/services/payoutService.js | 18 +- backend/sockets/messageSocket.js | 7 + backend/utils/logger.js | 30 +- frontend/src/components/AuthModal.tsx | 288 +++++++++--------- 15 files changed, 307 insertions(+), 173 deletions(-) diff --git a/backend/middleware/apiLogger.js b/backend/middleware/apiLogger.js index ce91996..8aa65d4 100644 --- a/backend/middleware/apiLogger.js +++ b/backend/middleware/apiLogger.js @@ -36,6 +36,34 @@ const apiLogger = (req, res, next) => { userId: req.user?.id || 'anonymous' }; + // Parse response body for error responses to include error details + if (res.statusCode >= 400) { + let errorDetails = null; + if (body) { + try { + const parsed = typeof body === 'string' ? JSON.parse(body) : body; + // Extract error message, validation errors, or full response + errorDetails = { + error: parsed.error || parsed.message || null, + errors: parsed.errors || null, // validation errors array + details: parsed.details || null + }; + // Remove null values + Object.keys(errorDetails).forEach(key => { + if (errorDetails[key] === null) delete errorDetails[key]; + }); + if (Object.keys(errorDetails).length > 0) { + responseData.errorDetails = errorDetails; + } + } catch (e) { + // Body is not JSON, include as string (truncated) + if (typeof body === 'string' && body.length > 0) { + responseData.errorDetails = { raw: body.substring(0, 500) }; + } + } + } + } + if (res.statusCode >= 400 && res.statusCode < 500) { // Don't log 401s for /users/profile - these are expected auth checks if (!(res.statusCode === 401 && req.url === '/profile')) { diff --git a/backend/middleware/rateLimiter.js b/backend/middleware/rateLimiter.js index c06ffaf..6329fc4 100644 --- a/backend/middleware/rateLimiter.js +++ b/backend/middleware/rateLimiter.js @@ -1,4 +1,5 @@ const rateLimit = require("express-rate-limit"); +const logger = require("../utils/logger"); // General rate limiter for Maps API endpoints const createMapsRateLimiter = (windowMs, max, message) => { @@ -111,6 +112,21 @@ const uploadPresignLimiter = createUserBasedRateLimiter( "Too many upload requests. Please slow down." ); +// Helper to create a rate limit handler that logs the event +const createRateLimitHandler = (limiterName) => (req, res, next, options) => { + const reqLogger = logger.withRequestId(req.id); + reqLogger.warn('Rate limit exceeded', { + limiter: limiterName, + ip: req.ip, + userId: req.user?.id || 'anonymous', + method: req.method, + url: req.url, + userAgent: req.get('User-Agent'), + message: options.message?.error || 'Rate limit exceeded' + }); + res.status(options.statusCode).json(options.message); +}; + // Authentication rate limiters const authRateLimiters = { // Login rate limiter - stricter to prevent brute force @@ -124,6 +140,7 @@ const authRateLimiters = { standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: true, // Don't count successful logins + handler: createRateLimitHandler('login'), }), // Registration rate limiter @@ -136,6 +153,7 @@ const authRateLimiters = { }, standardHeaders: true, legacyHeaders: false, + handler: createRateLimitHandler('register'), }), // Password reset rate limiter @@ -148,6 +166,7 @@ const authRateLimiters = { }, standardHeaders: true, legacyHeaders: false, + handler: createRateLimitHandler('passwordReset'), }), // Alpha code validation rate limiter @@ -160,6 +179,7 @@ const authRateLimiters = { }, standardHeaders: true, legacyHeaders: false, + handler: createRateLimitHandler('alphaCodeValidation'), }), // Email verification rate limiter - protect against brute force on 6-digit codes @@ -172,6 +192,7 @@ const authRateLimiters = { }, standardHeaders: true, legacyHeaders: false, + handler: createRateLimitHandler('emailVerification'), }), // General API rate limiter @@ -184,6 +205,7 @@ const authRateLimiters = { }, standardHeaders: true, legacyHeaders: false, + handler: createRateLimitHandler('general'), }), }; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 1077fab..95fb738 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -127,6 +127,7 @@ router.post( const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send verification email", { error: emailError.message, + stack: emailError.stack, userId: user.id, email: user.email, }); @@ -645,6 +646,7 @@ router.post( const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to resend verification email", { error: emailError.message, + stack: emailError.stack, userId: user.id, email: user.email, }); @@ -819,6 +821,7 @@ router.post( const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send password reset email", { error: emailError.message, + stack: emailError.stack, userId: user.id, email: user.email, }); @@ -960,6 +963,7 @@ router.post( const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send password changed notification", { error: emailError.message, + stack: emailError.stack, userId: user.id, email: user.email, }); diff --git a/backend/routes/conditionChecks.js b/backend/routes/conditionChecks.js index bfce062..d984c5a 100644 --- a/backend/routes/conditionChecks.js +++ b/backend/routes/conditionChecks.js @@ -57,6 +57,7 @@ router.post("/:rentalId", authenticateToken, async (req, res) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error submitting condition check", { error: error.message, + stack: error.stack, rentalId: req.params.rentalId, userId: req.user?.id, }); @@ -85,6 +86,7 @@ router.get("/:rentalId", authenticateToken, async (req, res) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error fetching condition checks", { error: error.message, + stack: error.stack, rentalId: req.params.rentalId, }); @@ -112,6 +114,7 @@ router.get("/:rentalId/timeline", authenticateToken, async (req, res) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error fetching condition check timeline", { error: error.message, + stack: error.stack, rentalId: req.params.rentalId, }); @@ -139,6 +142,7 @@ router.get("/", authenticateToken, async (req, res) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error fetching available checks", { error: error.message, + stack: error.stack, userId: req.user?.id, }); diff --git a/backend/routes/feedback.js b/backend/routes/feedback.js index 40086ae..a6d83e0 100644 --- a/backend/routes/feedback.js +++ b/backend/routes/feedback.js @@ -33,6 +33,7 @@ router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, } catch (emailError) { reqLogger.error("Failed to send feedback confirmation email", { error: emailError.message, + stack: emailError.stack, userId: req.user.id, feedbackId: feedback.id }); @@ -45,6 +46,7 @@ router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, } catch (emailError) { reqLogger.error("Failed to send feedback notification to admin", { error: emailError.message, + stack: emailError.stack, userId: req.user.id, feedbackId: feedback.id }); diff --git a/backend/routes/forum.js b/backend/routes/forum.js index a758f6b..dec14d1 100644 --- a/backend/routes/forum.js +++ b/backend/routes/forum.js @@ -311,6 +311,7 @@ router.post('/posts', authenticateToken, async (req, res, next) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Geocoding failed for item request", { error: error.message, + stack: error.stack, zipCode }); // Continue without coordinates - post will still be created @@ -450,6 +451,7 @@ router.post('/posts', authenticateToken, async (req, res, next) => { } catch (emailError) { logger.error("Failed to send item request notification", { error: emailError.message, + stack: emailError.stack, recipientId: user.id, postId: post.id }); @@ -1383,7 +1385,12 @@ router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, r } } catch (emailError) { // Log but don't fail the deletion - console.error('Failed to send forum post deletion notification email:', emailError.message); + logger.error('Failed to send forum post deletion notification email', { + error: emailError.message, + stack: emailError.stack, + postId: post.id, + authorId: post.authorId + }); } })(); @@ -1511,7 +1518,12 @@ router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req } } catch (emailError) { // Log but don't fail the deletion - console.error('Failed to send forum comment deletion notification email:', emailError.message); + logger.error('Failed to send forum comment deletion notification email', { + error: emailError.message, + stack: emailError.stack, + commentId: comment.id, + authorId: comment.authorId + }); } })(); diff --git a/backend/routes/items.js b/backend/routes/items.js index d75a7f5..2c784b4 100644 --- a/backend/routes/items.js +++ b/backend/routes/items.js @@ -372,10 +372,17 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res, next) itemWithOwner.owner, itemWithOwner ); - console.log(`First listing celebration email sent to owner ${req.user.id}`); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("First listing celebration email sent", { ownerId: req.user.id }); } catch (emailError) { // Log but don't fail the item creation - console.error('Failed to send first listing celebration email:', emailError.message); + const reqLogger = logger.withRequestId(req.id); + reqLogger.error('Failed to send first listing celebration email', { + error: emailError.message, + stack: emailError.stack, + ownerId: req.user.id, + itemId: item.id + }); } } @@ -571,10 +578,15 @@ router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res, ne item, reason.trim() ); - console.log(`Item deletion notification email sent to owner ${item.ownerId}`); + logger.info("Item deletion notification email sent", { ownerId: item.ownerId, itemId: item.id }); } catch (emailError) { // Log but don't fail the deletion - console.error('Failed to send item deletion notification email:', emailError.message); + logger.error('Failed to send item deletion notification email', { + error: emailError.message, + stack: emailError.stack, + ownerId: item.ownerId, + itemId: item.id + }); } const reqLogger = logger.withRequestId(req.id); diff --git a/backend/routes/messages.js b/backend/routes/messages.js index c731c08..0733b55 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -297,6 +297,7 @@ router.post('/', authenticateToken, async (req, res, next) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send message notification email", { error: emailError.message, + stack: emailError.stack, messageId: message.id, receiverId: receiverId }); @@ -436,6 +437,7 @@ router.get('/images/:filename', const reqLogger = logger.withRequestId(req.id); reqLogger.error('Image serve failed', { error: error.message, + stack: error.stack, filename: req.params.filename }); res.status(500).json({ error: 'Failed to load image' }); diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js index 120bac6..c5e3258 100644 --- a/backend/routes/rentals.js +++ b/backend/routes/rentals.js @@ -466,6 +466,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => { "Failed to send rental approval confirmation email to owner", { error: emailError.message, + stack: emailError.stack, rentalId: updatedRental.id, ownerId: updatedRental.ownerId, } @@ -505,6 +506,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => { "Failed to send rental confirmation email to renter", { error: emailError.message, + stack: emailError.stack, rentalId: updatedRental.id, renterId: updatedRental.renterId, } @@ -568,6 +570,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => { "Failed to send rental approval confirmation email to owner", { error: emailError.message, + stack: emailError.stack, rentalId: updatedRental.id, ownerId: updatedRental.ownerId, } @@ -607,6 +610,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => { "Failed to send rental confirmation email to renter", { error: emailError.message, + stack: emailError.stack, rentalId: updatedRental.id, renterId: updatedRental.renterId, } @@ -1112,6 +1116,7 @@ router.post("/:id/cancel", authenticateToken, async (req, res, next) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send cancellation emails", { error: emailError.message, + stack: emailError.stack, rentalId: updatedRental.id, cancelledBy: updatedRental.cancelledBy, }); @@ -1206,6 +1211,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send rental completion emails", { error: emailError.message, + stack: emailError.stack, rentalId, }); } @@ -1294,6 +1300,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error marking return status", { error: error.message, + stack: error.stack, rentalId: req.params.id, userId: req.user.id, }); @@ -1376,6 +1383,7 @@ router.post("/:id/report-damage", authenticateToken, async (req, res, next) => { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Error reporting damage", { error: error.message, + stack: error.stack, rentalId: req.params.id, userId: req.user.id, }); diff --git a/backend/services/UserService.js b/backend/services/UserService.js index a980abc..4a8ca1e 100644 --- a/backend/services/UserService.js +++ b/backend/services/UserService.js @@ -89,6 +89,7 @@ class UserService { "Failed to send personal information changed notification", { error: emailError.message, + stack: emailError.stack, userId: user.id, email: user.email, changedFields, @@ -138,6 +139,7 @@ class UserService { } catch (emailError) { logger.error("Failed to send notification for address creation", { error: emailError.message, + stack: emailError.stack, userId: user.id, addressId: address.id, }); @@ -181,6 +183,7 @@ class UserService { } catch (emailError) { logger.error("Failed to send notification for address update", { error: emailError.message, + stack: emailError.stack, userId, addressId: address.id, }); @@ -223,6 +226,7 @@ class UserService { } catch (emailError) { logger.error("Failed to send notification for address deletion", { error: emailError.message, + stack: emailError.stack, userId, addressId, }); diff --git a/backend/services/email/domain/RentalFlowEmailService.js b/backend/services/email/domain/RentalFlowEmailService.js index b4a957d..3e59d35 100644 --- a/backend/services/email/domain/RentalFlowEmailService.js +++ b/backend/services/email/domain/RentalFlowEmailService.js @@ -1,5 +1,6 @@ const EmailClient = require("../core/EmailClient"); const TemplateManager = require("../core/TemplateManager"); +const logger = require("../../../utils/logger"); /** * RentalFlowEmailService handles rental lifecycle flow emails @@ -997,10 +998,12 @@ class RentalFlowEmailService { ); } } catch (emailError) { - console.error( - `Failed to send rental completion email to renter (${renter.email}):`, - emailError.message - ); + logger.error("Failed to send rental completion email to renter", { + error: emailError.message, + stack: emailError.stack, + renterEmail: renter.email, + rentalId: rental.id + }); } // Prepare owner email @@ -1114,13 +1117,19 @@ class RentalFlowEmailService { ); } } catch (emailError) { - console.error( - `Failed to send rental completion email to owner (${owner.email}):`, - emailError.message - ); + logger.error("Failed to send rental completion email to owner", { + error: emailError.message, + stack: emailError.stack, + ownerEmail: owner.email, + rentalId: rental.id + }); } } catch (error) { - console.error("Error sending rental completion emails:", error); + logger.error("Error sending rental completion emails", { + error: error.message, + stack: error.stack, + rentalId: rental?.id + }); } return results; diff --git a/backend/services/payoutService.js b/backend/services/payoutService.js index 6a703ed..497faaa 100644 --- a/backend/services/payoutService.js +++ b/backend/services/payoutService.js @@ -1,6 +1,7 @@ const { Rental, User, Item } = require("../models"); const StripeService = require("./stripeService"); const emailServices = require("./email"); +const logger = require("../utils/logger"); const { Op } = require("sequelize"); class PayoutService { @@ -78,15 +79,18 @@ class PayoutService { // Send payout notification email to owner try { await emailServices.rentalFlow.sendPayoutReceivedEmail(rental.owner, rental); - console.log( - `Payout notification email sent to owner for rental ${rental.id}` - ); + logger.info("Payout notification email sent to owner", { + rentalId: rental.id, + ownerId: rental.ownerId + }); } catch (emailError) { // Log error but don't fail the payout - console.error( - `Failed to send payout notification email for rental ${rental.id}:`, - emailError.message - ); + logger.error("Failed to send payout notification email", { + error: emailError.message, + stack: emailError.stack, + rentalId: rental.id, + ownerId: rental.ownerId + }); } return { diff --git a/backend/sockets/messageSocket.js b/backend/sockets/messageSocket.js index 2dc271e..c68e2fa 100644 --- a/backend/sockets/messageSocket.js +++ b/backend/sockets/messageSocket.js @@ -94,6 +94,7 @@ const initializeMessageSocket = (io) => { socketId: socket.id, userId, error: error.message, + stack: error.stack, }); } }); @@ -124,6 +125,7 @@ const initializeMessageSocket = (io) => { socketId: socket.id, userId, error: error.message, + stack: error.stack, }); } }); @@ -170,6 +172,7 @@ const initializeMessageSocket = (io) => { socketId: socket.id, userId, error: error.message, + stack: error.stack, }); } }); @@ -208,6 +211,7 @@ const initializeMessageSocket = (io) => { socketId: socket.id, userId, error: error.message, + stack: error.stack, }); } }); @@ -243,6 +247,7 @@ const initializeMessageSocket = (io) => { socketId: socket.id, userId, error: error.message, + stack: error.stack, }); } }); @@ -305,6 +310,7 @@ const emitNewMessage = (io, receiverId, messageData) => { receiverId, messageId: messageData.id, error: error.message, + stack: error.stack, }); } }; @@ -330,6 +336,7 @@ const emitMessageRead = (io, senderId, readData) => { senderId, messageId: readData.messageId, error: error.message, + stack: error.stack, }); } }; diff --git a/backend/utils/logger.js b/backend/utils/logger.js index db6a264..56817b4 100644 --- a/backend/utils/logger.js +++ b/backend/utils/logger.js @@ -49,17 +49,29 @@ const logFormat = winston.format.combine( const { timestamp, level, message, stack, ...metadata } = info; let output = `${timestamp} ${level}: ${message}`; - // Check for stack trace in the info object itself - if (stack) { - output += `\n${stack}`; + // Include relevant metadata in console output + const metaKeys = Object.keys(metadata).filter(key => + !['splat', 'Symbol(level)', 'Symbol(message)'].includes(key) && + !key.startsWith('Symbol') + ); + + if (metaKeys.length > 0) { + const metaOutput = {}; + metaKeys.forEach(key => { + // For Error objects, extract message and stack + if (metadata[key] && metadata[key].stack) { + metaOutput[key] = { message: metadata[key].message, stack: metadata[key].stack }; + } else { + metaOutput[key] = metadata[key]; + } + }); + output += ` ${JSON.stringify(metaOutput, null, 2)}`; } - // Check for Error objects in metadata - Object.keys(metadata).forEach(key => { - if (metadata[key] && metadata[key].stack) { - output += `\n${key}: ${metadata[key].message}\n${metadata[key].stack}`; - } - }); + // Check for stack trace in the info object itself + if (stack) { + output += `\nStack: ${stack}`; + } return output; }), diff --git a/frontend/src/components/AuthModal.tsx b/frontend/src/components/AuthModal.tsx index a3c2550..bcdb42d 100644 --- a/frontend/src/components/AuthModal.tsx +++ b/frontend/src/components/AuthModal.tsx @@ -74,7 +74,7 @@ const AuthModal: React.FC = ({ // Focus the first input element when modal opens if (modalRef.current) { - const firstInput = modalRef.current.querySelector('input'); + const firstInput = modalRef.current.querySelector("input"); firstInput?.focus(); } @@ -96,11 +96,12 @@ const AuthModal: React.FC = ({ const handleGoogleLogin = () => { const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID; const redirectUri = `${window.location.origin}/auth/google/callback`; - const scope = 'openid email profile'; - const responseType = 'code'; + const scope = "openid email profile"; + const responseType = "code"; - const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?` + - `client_id=${encodeURIComponent(clientId || '')}` + + const googleAuthUrl = + `https://accounts.google.com/o/oauth2/v2/auth?` + + `client_id=${encodeURIComponent(clientId || "")}` + `&redirect_uri=${encodeURIComponent(redirectUri)}` + `&response_type=${responseType}` + `&scope=${encodeURIComponent(scope)}` + @@ -158,13 +159,14 @@ const AuthModal: React.FC = ({ // Show first specific validation error if available, otherwise generic error const details = err.response?.data?.details; const specificError = details?.[0]?.message; - setError(specificError || err.response?.data?.error || "An error occurred"); + setError( + specificError || err.response?.data?.error || "An error occurred" + ); } finally { setLoading(false); } }; - if (!show && !showForgotPassword && !showVerificationModal) return null; return ( @@ -195,158 +197,160 @@ const AuthModal: React.FC = ({ >
-
- -
-
-

- Welcome to Village Share -

+
+ +
+
+

Welcome to Village Share

- {error && ( -
- {error} -
- )} - - {/* Email Form */} -
- {mode === "signup" && ( - <> -
-
- - setFirstName(e.target.value)} - /> -
-
- - setLastName(e.target.value)} - /> -
-
- - )} - -
- - setEmail(e.target.value)} - placeholder="name@example.com" - autoComplete="email" - /> -
- - setPassword(e.target.value)} - required - /> - {mode === "signup" && ( -
- + {error && ( +
+ {error}
)} - {mode === "login" && ( -
+ {/* Email Form */} + + {mode === "signup" && ( + <> +
+
+ + setFirstName(e.target.value)} + /> +
+
+ + setLastName(e.target.value)} + /> +
+
+ + )} + +
+ + setEmail(e.target.value)} + autoComplete="email" + /> +
+ + setPassword(e.target.value)} + required + /> + {mode === "signup" && ( +
+ +
+ )} + + {mode === "login" && ( + + )} + + + + +
+
+ or +
+
+ + {/* Social Login Options */} + + + - )} + +
- - - -
-
- or -
-
- - {/* Social Login Options */} - - - - -

- By continuing, you agree to Village Share's{" "} - - Terms of Service - {" "} - and{" "} - - Privacy Policy - - . -

-
)} {/* Forgot Password Modal */}