Files
rentall-app/backend/routes/users.js
2026-01-15 16:11:57 -05:00

411 lines
12 KiB
JavaScript

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;