266 lines
7.4 KiB
JavaScript
266 lines
7.4 KiB
JavaScript
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 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, 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, 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', async (req, res, next) => {
|
|
try {
|
|
const user = await User.findByPk(req.params.id, {
|
|
attributes: { exclude: ['password', 'email', 'phone', 'address'] }
|
|
});
|
|
|
|
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
|
|
});
|
|
|
|
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) {
|
|
console.error('Profile update error:', error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
module.exports = router; |