const express = require('express'); const { User, UserAddress } = require('../models'); // Import from models/index.js to get models with associations const { authenticateToken, optionalAuth, requireAdmin } = require('../middleware/auth'); const { validateCoordinatesBody, handleValidationErrors } = require('../middleware/validation'); const logger = require('../utils/logger'); const userService = require('../services/UserService'); const { validateS3Keys } = require('../utils/s3KeyValidator'); const { IMAGE_LIMITS } = require('../config/imageLimits'); const router = express.Router(); // Allowed fields for profile update (prevents mass assignment) const ALLOWED_PROFILE_FIELDS = [ 'firstName', 'lastName', 'email', 'phone', 'address1', 'address2', 'city', 'state', 'zipCode', 'country', 'imageFilename', 'itemRequestNotificationRadius', ]; // Allowed fields for user address create/update (prevents mass assignment) const ALLOWED_ADDRESS_FIELDS = [ 'address1', 'address2', 'city', 'state', 'zipCode', 'country', 'latitude', 'longitude', ]; /** * Extract only allowed fields from request body */ function extractAllowedProfileFields(body) { const result = {}; for (const field of ALLOWED_PROFILE_FIELDS) { if (body[field] !== undefined) { result[field] = body[field]; } } return result; } /** * Extract only allowed address fields from request body */ function extractAllowedAddressFields(body) { const result = {}; for (const field of ALLOWED_ADDRESS_FIELDS) { if (body[field] !== undefined) { result[field] = body[field]; } } return result; } router.get('/profile', authenticateToken, async (req, res, next) => { try { const user = await User.findByPk(req.user.id, { attributes: { exclude: ['password'] } }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("User profile fetched", { userId: req.user.id }); res.json(user); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("User profile fetch failed", { error: error.message, stack: error.stack, userId: req.user.id }); next(error); } }); // Address routes (must come before /:id route) router.get('/addresses', authenticateToken, async (req, res, next) => { try { const addresses = await UserAddress.findAll({ where: { userId: req.user.id }, order: [['isPrimary', 'DESC'], ['createdAt', 'ASC']] }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("User addresses fetched", { userId: req.user.id, addressCount: addresses.length }); res.json(addresses); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("User addresses fetch failed", { error: error.message, stack: error.stack, userId: req.user.id }); next(error); } }); router.post('/addresses', authenticateToken, ...validateCoordinatesBody, handleValidationErrors, async (req, res, next) => { try { // Extract only allowed fields (prevents mass assignment) const allowedData = extractAllowedAddressFields(req.body); const address = await userService.createUserAddress(req.user.id, allowedData); res.status(201).json(address); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("User address creation failed", { error: error.message, stack: error.stack, userId: req.user.id, addressData: logger.sanitize(req.body) }); next(error); } }); router.put('/addresses/:id', authenticateToken, ...validateCoordinatesBody, handleValidationErrors, async (req, res, next) => { try { // Extract only allowed fields (prevents mass assignment) const allowedData = extractAllowedAddressFields(req.body); const address = await userService.updateUserAddress(req.user.id, req.params.id, allowedData); res.json(address); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("User address update failed", { error: error.message, stack: error.stack, userId: req.user.id, addressId: req.params.id }); if (error.message === 'Address not found') { return res.status(404).json({ error: 'Address not found' }); } next(error); } }); router.delete('/addresses/:id', authenticateToken, async (req, res, next) => { try { await userService.deleteUserAddress(req.user.id, req.params.id); res.status(204).send(); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("User address deletion failed", { error: error.message, stack: error.stack, userId: req.user.id, addressId: req.params.id }); if (error.message === 'Address not found') { return res.status(404).json({ error: 'Address not found' }); } next(error); } }); // User availability routes (must come before /:id route) router.get('/availability', authenticateToken, async (req, res, next) => { try { const user = await User.findByPk(req.user.id, { attributes: ['defaultAvailableAfter', 'defaultAvailableBefore', 'defaultSpecifyTimesPerDay', 'defaultWeeklyTimes'] }); res.json({ generalAvailableAfter: user.defaultAvailableAfter, generalAvailableBefore: user.defaultAvailableBefore, specifyTimesPerDay: user.defaultSpecifyTimesPerDay, weeklyTimes: user.defaultWeeklyTimes }); } catch (error) { next(error); } }); router.put('/availability', authenticateToken, async (req, res, next) => { try { const { generalAvailableAfter, generalAvailableBefore, specifyTimesPerDay, weeklyTimes } = req.body; await User.update({ defaultAvailableAfter: generalAvailableAfter, defaultAvailableBefore: generalAvailableBefore, defaultSpecifyTimesPerDay: specifyTimesPerDay, defaultWeeklyTimes: weeklyTimes }, { where: { id: req.user.id } }); res.json({ message: 'Availability updated successfully' }); } catch (error) { next(error); } }); router.get('/:id', optionalAuth, async (req, res, next) => { try { const isAdmin = req.user?.role === 'admin'; // Base attributes to exclude const excludedAttributes = ['password', 'email', 'phone', 'address', 'verificationToken', 'passwordResetToken']; // If not admin, also exclude ban-related fields if (!isAdmin) { excludedAttributes.push('isBanned', 'bannedAt', 'bannedBy', 'banReason'); } const user = await User.findByPk(req.params.id, { attributes: { exclude: excludedAttributes } }); if (!user) { return res.status(404).json({ error: 'User not found' }); } const reqLogger = logger.withRequestId(req.id); reqLogger.info("Public user profile fetched", { requestedUserId: req.params.id, viewerIsAdmin: isAdmin }); res.json(user); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Public user profile fetch failed", { error: error.message, stack: error.stack, requestedUserId: req.params.id }); next(error); } }); router.put('/profile', authenticateToken, async (req, res, next) => { try { // Extract only allowed fields (prevents mass assignment) const allowedData = extractAllowedProfileFields(req.body); // Validate imageFilename if provided if (allowedData.imageFilename !== undefined && allowedData.imageFilename !== null) { const keyValidation = validateS3Keys([allowedData.imageFilename], 'profiles', { maxKeys: IMAGE_LIMITS.profile }); if (!keyValidation.valid) { return res.status(400).json({ error: keyValidation.error, details: keyValidation.invalidKeys }); } } // Use UserService to handle update and email notification const updatedUser = await userService.updateProfile(req.user.id, allowedData); res.json(updatedUser); } catch (error) { logger.error('Profile update error', { error }); next(error); } }); // Admin: Ban a user router.post('/admin/:id/ban', authenticateToken, requireAdmin, async (req, res, next) => { try { const { reason } = req.body; const targetUserId = req.params.id; // Validate reason is provided if (!reason || !reason.trim()) { return res.status(400).json({ error: "Ban reason is required" }); } // Prevent banning yourself if (targetUserId === req.user.id) { return res.status(400).json({ error: "You cannot ban yourself" }); } const targetUser = await User.findByPk(targetUserId); if (!targetUser) { return res.status(404).json({ error: "User not found" }); } // Prevent banning other admins if (targetUser.role === 'admin') { return res.status(403).json({ error: "Cannot ban admin users" }); } // Check if already banned if (targetUser.isBanned) { return res.status(400).json({ error: "User is already banned" }); } // Ban the user (this also invalidates sessions via jwtVersion increment) await targetUser.banUser(req.user.id, reason.trim()); // Send ban notification email try { const emailServices = require("../services/email"); await emailServices.userEngagement.sendUserBannedNotification( targetUser, req.user, reason.trim() ); const reqLogger = logger.withRequestId(req.id); reqLogger.info("User ban notification email sent", { bannedUserId: targetUserId, adminId: req.user.id }); } catch (emailError) { // Log but don't fail the ban operation const reqLogger = logger.withRequestId(req.id); reqLogger.error('Failed to send user ban notification email', { error: emailError.message, stack: emailError.stack, bannedUserId: targetUserId, adminId: req.user.id }); } const reqLogger = logger.withRequestId(req.id); reqLogger.info("User banned by admin", { targetUserId, adminId: req.user.id, reason: reason.trim() }); // Return updated user data (excluding sensitive fields) const updatedUser = await User.findByPk(targetUserId, { attributes: { exclude: ['password', 'verificationToken', 'passwordResetToken'] } }); res.json({ message: "User has been banned successfully", user: updatedUser }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Admin ban user failed", { error: error.message, stack: error.stack, targetUserId: req.params.id, adminId: req.user.id }); next(error); } }); // Admin: Unban a user router.post('/admin/:id/unban', authenticateToken, requireAdmin, async (req, res, next) => { try { const targetUserId = req.params.id; const targetUser = await User.findByPk(targetUserId); if (!targetUser) { return res.status(404).json({ error: "User not found" }); } // Check if user is actually banned if (!targetUser.isBanned) { return res.status(400).json({ error: "User is not banned" }); } // Unban the user await targetUser.unbanUser(); const reqLogger = logger.withRequestId(req.id); reqLogger.info("User unbanned by admin", { targetUserId, adminId: req.user.id }); // Return updated user data (excluding sensitive fields) const updatedUser = await User.findByPk(targetUserId, { attributes: { exclude: ['password', 'verificationToken', 'passwordResetToken'] } }); res.json({ message: "User has been unbanned successfully", user: updatedUser }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Admin unban user failed", { error: error.message, stack: error.stack, targetUserId: req.params.id, adminId: req.user.id }); next(error); } }); module.exports = router;