s3 image file validation
This commit is contained in:
@@ -2,6 +2,8 @@ const express = require("express");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const ConditionCheckService = require("../services/conditionCheckService");
|
||||
const logger = require("../utils/logger");
|
||||
const { validateS3Keys } = require("../utils/s3KeyValidator");
|
||||
const { IMAGE_LIMITS } = require("../config/imageLimits");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -13,10 +15,24 @@ router.post("/:rentalId", authenticateToken, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
|
||||
// Ensure imageFilenames is an array (S3 keys)
|
||||
const imageFilenames = Array.isArray(rawImageFilenames)
|
||||
const imageFilenamesArray = Array.isArray(rawImageFilenames)
|
||||
? rawImageFilenames
|
||||
: [];
|
||||
|
||||
// Validate S3 keys format and folder
|
||||
const keyValidation = validateS3Keys(imageFilenamesArray, "condition-checks", {
|
||||
maxKeys: IMAGE_LIMITS.conditionChecks,
|
||||
});
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: keyValidation.error,
|
||||
details: keyValidation.invalidKeys,
|
||||
});
|
||||
}
|
||||
|
||||
const imageFilenames = imageFilenamesArray;
|
||||
|
||||
const conditionCheck = await ConditionCheckService.submitConditionCheck(
|
||||
rentalId,
|
||||
checkType,
|
||||
|
||||
@@ -6,6 +6,8 @@ const logger = require('../utils/logger');
|
||||
const emailServices = require('../services/email');
|
||||
const googleMapsService = require('../services/googleMapsService');
|
||||
const locationService = require('../services/locationService');
|
||||
const { validateS3Keys } = require('../utils/s3KeyValidator');
|
||||
const { IMAGE_LIMITS } = require('../config/imageLimits');
|
||||
const router = express.Router();
|
||||
|
||||
// Helper function to build nested comment tree
|
||||
@@ -239,10 +241,20 @@ router.get('/posts/:id', optionalAuth, async (req, res, next) => {
|
||||
// POST /api/forum/posts - Create new post
|
||||
router.post('/posts', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng, imageFilenames } = req.body;
|
||||
let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng, imageFilenames: rawImageFilenames } = req.body;
|
||||
|
||||
// Ensure imageFilenames is an array
|
||||
imageFilenames = Array.isArray(imageFilenames) ? imageFilenames : [];
|
||||
// Ensure imageFilenames is an array and validate S3 keys
|
||||
const imageFilenamesArray = Array.isArray(rawImageFilenames) ? rawImageFilenames : [];
|
||||
|
||||
const keyValidation = validateS3Keys(imageFilenamesArray, 'forum', { maxKeys: IMAGE_LIMITS.forum });
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: keyValidation.error,
|
||||
details: keyValidation.invalidKeys
|
||||
});
|
||||
}
|
||||
|
||||
const imageFilenames = imageFilenamesArray;
|
||||
|
||||
// Initialize location fields
|
||||
let latitude = null;
|
||||
@@ -488,9 +500,26 @@ router.put('/posts/:id', authenticateToken, async (req, res, next) => {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { title, content, category, tags } = req.body;
|
||||
const { title, content, category, tags, imageFilenames: rawImageFilenames } = req.body;
|
||||
|
||||
await post.update({ title, content, category });
|
||||
// Build update object
|
||||
const updateData = { title, content, category };
|
||||
|
||||
// Handle imageFilenames if provided
|
||||
if (rawImageFilenames !== undefined) {
|
||||
const imageFilenamesArray = Array.isArray(rawImageFilenames) ? rawImageFilenames : [];
|
||||
|
||||
const keyValidation = validateS3Keys(imageFilenamesArray, 'forum', { maxKeys: IMAGE_LIMITS.forum });
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: keyValidation.error,
|
||||
details: keyValidation.invalidKeys
|
||||
});
|
||||
}
|
||||
updateData.imageFilenames = imageFilenamesArray;
|
||||
}
|
||||
|
||||
await post.update(updateData);
|
||||
|
||||
// Update tags if provided
|
||||
if (tags !== undefined) {
|
||||
@@ -927,8 +956,18 @@ router.post('/posts/:id/comments', authenticateToken, async (req, res, next) =>
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure imageFilenames is an array
|
||||
const imageFilenames = Array.isArray(rawImageFilenames) ? rawImageFilenames : [];
|
||||
// Ensure imageFilenames is an array and validate S3 keys
|
||||
const imageFilenamesArray = Array.isArray(rawImageFilenames) ? rawImageFilenames : [];
|
||||
|
||||
const keyValidation = validateS3Keys(imageFilenamesArray, 'forum', { maxKeys: IMAGE_LIMITS.forum });
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: keyValidation.error,
|
||||
details: keyValidation.invalidKeys
|
||||
});
|
||||
}
|
||||
|
||||
const imageFilenames = imageFilenamesArray;
|
||||
|
||||
const comment = await ForumComment.create({
|
||||
postId: req.params.id,
|
||||
|
||||
@@ -3,8 +3,56 @@ const { Op } = require("sequelize");
|
||||
const { Item, User, Rental } = require("../models"); // Import from models/index.js to get models with associations
|
||||
const { authenticateToken, requireVerifiedEmail, requireAdmin, optionalAuth } = require("../middleware/auth");
|
||||
const logger = require("../utils/logger");
|
||||
const { validateS3Keys } = require("../utils/s3KeyValidator");
|
||||
const { IMAGE_LIMITS } = require("../config/imageLimits");
|
||||
const router = express.Router();
|
||||
|
||||
// Allowed fields for item create/update (prevents mass assignment)
|
||||
const ALLOWED_ITEM_FIELDS = [
|
||||
'name',
|
||||
'description',
|
||||
'pickUpAvailable',
|
||||
'localDeliveryAvailable',
|
||||
'localDeliveryRadius',
|
||||
'shippingAvailable',
|
||||
'inPlaceUseAvailable',
|
||||
'pricePerHour',
|
||||
'pricePerDay',
|
||||
'pricePerWeek',
|
||||
'pricePerMonth',
|
||||
'replacementCost',
|
||||
'address1',
|
||||
'address2',
|
||||
'city',
|
||||
'state',
|
||||
'zipCode',
|
||||
'country',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'imageFilenames',
|
||||
'isAvailable',
|
||||
'rules',
|
||||
'availableAfter',
|
||||
'availableBefore',
|
||||
'specifyTimesPerDay',
|
||||
'weeklyTimes',
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract only allowed fields from request body
|
||||
* @param {Object} body - Request body
|
||||
* @returns {Object} - Object with only allowed fields
|
||||
*/
|
||||
function extractAllowedFields(body) {
|
||||
const result = {};
|
||||
for (const field of ALLOWED_ITEM_FIELDS) {
|
||||
if (body[field] !== undefined) {
|
||||
result[field] = body[field];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
router.get("/", async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
@@ -232,8 +280,27 @@ router.get("/:id", optionalAuth, async (req, res, next) => {
|
||||
|
||||
router.post("/", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
|
||||
try {
|
||||
// Extract only allowed fields (prevents mass assignment)
|
||||
const allowedData = extractAllowedFields(req.body);
|
||||
|
||||
// Validate imageFilenames if provided
|
||||
if (allowedData.imageFilenames) {
|
||||
const imageFilenames = Array.isArray(allowedData.imageFilenames)
|
||||
? allowedData.imageFilenames
|
||||
: [];
|
||||
|
||||
const keyValidation = validateS3Keys(imageFilenames, 'items', { maxKeys: IMAGE_LIMITS.items });
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: keyValidation.error,
|
||||
details: keyValidation.invalidKeys
|
||||
});
|
||||
}
|
||||
allowedData.imageFilenames = imageFilenames;
|
||||
}
|
||||
|
||||
const item = await Item.create({
|
||||
...req.body,
|
||||
...allowedData,
|
||||
ownerId: req.user.id,
|
||||
});
|
||||
|
||||
@@ -300,7 +367,26 @@ router.put("/:id", authenticateToken, async (req, res, next) => {
|
||||
return res.status(403).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
await item.update(req.body);
|
||||
// Extract only allowed fields (prevents mass assignment)
|
||||
const allowedData = extractAllowedFields(req.body);
|
||||
|
||||
// Validate imageFilenames if provided
|
||||
if (allowedData.imageFilenames !== undefined) {
|
||||
const imageFilenames = Array.isArray(allowedData.imageFilenames)
|
||||
? allowedData.imageFilenames
|
||||
: [];
|
||||
|
||||
const keyValidation = validateS3Keys(imageFilenames, 'items', { maxKeys: IMAGE_LIMITS.items });
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: keyValidation.error,
|
||||
details: keyValidation.invalidKeys
|
||||
});
|
||||
}
|
||||
allowedData.imageFilenames = imageFilenames;
|
||||
}
|
||||
|
||||
await item.update(allowedData);
|
||||
|
||||
const updatedItem = await Item.findByPk(item.id, {
|
||||
include: [
|
||||
|
||||
@@ -8,6 +8,8 @@ const { Op } = require('sequelize');
|
||||
const emailServices = require('../services/email');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { validateS3Keys } = require('../utils/s3KeyValidator');
|
||||
const { IMAGE_LIMITS } = require('../config/imageLimits');
|
||||
const router = express.Router();
|
||||
|
||||
// Get all messages for the current user (inbox)
|
||||
@@ -240,6 +242,17 @@ router.post('/', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const { receiverId, content, imageFilename } = req.body;
|
||||
|
||||
// Validate imageFilename if provided
|
||||
if (imageFilename) {
|
||||
const keyValidation = validateS3Keys([imageFilename], 'messages', { maxKeys: IMAGE_LIMITS.messages });
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: keyValidation.error,
|
||||
details: keyValidation.invalidKeys
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if receiver exists
|
||||
const receiver = await User.findByPk(receiverId);
|
||||
if (!receiver) {
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
const express = require('express');
|
||||
const { User, UserAddress } = require('../models'); // Import from models/index.js to get models with associations
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const { uploadProfileImage } = require('../middleware/upload');
|
||||
const logger = require('../utils/logger');
|
||||
const userService = require('../services/UserService');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
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',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
router.get('/profile', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id, {
|
||||
@@ -182,8 +210,22 @@ router.get('/:id', async (req, res, next) => {
|
||||
|
||||
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, req.body);
|
||||
const updatedUser = await userService.updateProfile(req.user.id, allowedData);
|
||||
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
@@ -192,65 +234,4 @@ router.put('/profile', authenticateToken, async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Upload profile image endpoint
|
||||
router.post('/profile/image', authenticateToken, (req, res) => {
|
||||
uploadProfileImage(req, res, async (err) => {
|
||||
if (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 });
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete old profile image if exists
|
||||
const user = await User.findByPk(req.user.id);
|
||||
if (user.imageFilename) {
|
||||
const oldImagePath = path.join(__dirname, '../uploads/profiles', user.imageFilename);
|
||||
try {
|
||||
await fs.unlink(oldImagePath);
|
||||
} catch (unlinkErr) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.warn("Error deleting old profile image", {
|
||||
error: unlinkErr.message,
|
||||
userId: req.user.id,
|
||||
oldImagePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update user with new image filename
|
||||
await user.update({
|
||||
imageFilename: req.file.filename
|
||||
});
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Profile image uploaded successfully", {
|
||||
userId: req.user.id,
|
||||
filename: req.file.filename
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Profile image uploaded successfully',
|
||||
filename: req.file.filename,
|
||||
imageUrl: `/uploads/profiles/${req.file.filename}`
|
||||
});
|
||||
} catch (error) {
|
||||
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' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user