720 lines
21 KiB
JavaScript
720 lines
21 KiB
JavaScript
const express = require("express");
|
|
const { Op, Sequelize } = require("sequelize");
|
|
const { Item, User, Rental, sequelize } = require("../models"); // Import from models/index.js to get models with associations
|
|
const { authenticateToken, requireVerifiedEmail, requireAdmin, optionalAuth } = require("../middleware/auth");
|
|
const { validateCoordinatesQuery, validateCoordinatesBody, handleValidationErrors } = require("../middleware/validation");
|
|
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("/", validateCoordinatesQuery, async (req, res, next) => {
|
|
try {
|
|
const {
|
|
minPrice,
|
|
maxPrice,
|
|
city,
|
|
zipCode,
|
|
search,
|
|
lat,
|
|
lng,
|
|
radius = 25,
|
|
page = 1,
|
|
limit = 20,
|
|
} = req.query;
|
|
|
|
const where = {
|
|
isDeleted: false // Always exclude soft-deleted items from public browse
|
|
};
|
|
|
|
if (minPrice || maxPrice) {
|
|
where.pricePerDay = {};
|
|
if (minPrice) where.pricePerDay[Op.gte] = minPrice;
|
|
if (maxPrice) where.pricePerDay[Op.lte] = maxPrice;
|
|
}
|
|
|
|
// Location filtering: Radius search OR city/ZIP fallback
|
|
if (lat && lng) {
|
|
// Parse and validate coordinates
|
|
const latNum = parseFloat(lat);
|
|
const lngNum = parseFloat(lng);
|
|
const radiusNum = parseFloat(radius);
|
|
|
|
if (!isNaN(latNum) && !isNaN(lngNum) && !isNaN(radiusNum)) {
|
|
// Bounding box pre-filter (fast, uses indexes)
|
|
// ~69 miles per degree latitude, longitude varies by latitude
|
|
const latDelta = radiusNum / 69;
|
|
const lngDelta = radiusNum / (69 * Math.cos(latNum * Math.PI / 180));
|
|
|
|
where.latitude = {
|
|
[Op.and]: [
|
|
{ [Op.gte]: latNum - latDelta },
|
|
{ [Op.lte]: latNum + latDelta },
|
|
{ [Op.ne]: null }
|
|
]
|
|
};
|
|
where.longitude = {
|
|
[Op.and]: [
|
|
{ [Op.gte]: lngNum - lngDelta },
|
|
{ [Op.lte]: lngNum + lngDelta },
|
|
{ [Op.ne]: null }
|
|
]
|
|
};
|
|
|
|
// Haversine formula for exact distance (applied after bounding box)
|
|
// 3959 = Earth's radius in miles
|
|
where[Op.and] = sequelize.literal(`
|
|
(3959 * acos(
|
|
cos(radians(${latNum})) * cos(radians("Item"."latitude")) *
|
|
cos(radians("Item"."longitude") - radians(${lngNum})) +
|
|
sin(radians(${latNum})) * sin(radians("Item"."latitude"))
|
|
)) <= ${radiusNum}
|
|
`);
|
|
}
|
|
} else {
|
|
// Fallback to city/ZIP string matching
|
|
if (city) where.city = { [Op.iLike]: `%${city}%` };
|
|
if (zipCode) where.zipCode = { [Op.iLike]: `%${zipCode}%` };
|
|
}
|
|
if (search) {
|
|
where[Op.or] = [
|
|
{ name: { [Op.iLike]: `%${search}%` } },
|
|
{ description: { [Op.iLike]: `%${search}%` } },
|
|
];
|
|
}
|
|
|
|
const offset = (page - 1) * limit;
|
|
|
|
const { count, rows } = await Item.findAndCountAll({
|
|
where,
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: "owner",
|
|
attributes: ["id", "firstName", "lastName", "imageFilename"],
|
|
where: {
|
|
isBanned: { [Op.ne]: true }
|
|
},
|
|
required: true,
|
|
},
|
|
],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
order: [["createdAt", "DESC"]],
|
|
});
|
|
|
|
// Round coordinates to 2 decimal places for map display while keeping precise values in database
|
|
const itemsWithRoundedCoords = rows.map(item => {
|
|
const itemData = item.toJSON();
|
|
if (itemData.latitude !== null && itemData.latitude !== undefined) {
|
|
itemData.latitude = Math.round(parseFloat(itemData.latitude) * 100) / 100;
|
|
}
|
|
if (itemData.longitude !== null && itemData.longitude !== undefined) {
|
|
itemData.longitude = Math.round(parseFloat(itemData.longitude) * 100) / 100;
|
|
}
|
|
return itemData;
|
|
});
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Items search completed", {
|
|
filters: { minPrice, maxPrice, city, zipCode, search, lat, lng, radius },
|
|
resultsCount: count,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit)
|
|
});
|
|
|
|
res.json({
|
|
items: itemsWithRoundedCoords,
|
|
totalPages: Math.ceil(count / limit),
|
|
currentPage: parseInt(page),
|
|
totalItems: count,
|
|
});
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Items search failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
query: req.query
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.get("/recommendations", authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const userRentals = await Rental.findAll({
|
|
where: { renterId: req.user.id },
|
|
include: [{ model: Item, as: "item" }],
|
|
});
|
|
|
|
// For now, just return random available items as recommendations
|
|
const recommendations = await Item.findAll({
|
|
where: {
|
|
isAvailable: true,
|
|
isDeleted: false,
|
|
},
|
|
limit: 10,
|
|
order: [["createdAt", "DESC"]],
|
|
});
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Recommendations fetched", {
|
|
userId: req.user.id,
|
|
recommendationsCount: recommendations.length
|
|
});
|
|
|
|
res.json(recommendations);
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Recommendations fetch failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
userId: req.user.id
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Public endpoint to get reviews for a specific item (must come before /:id route)
|
|
router.get('/:id/reviews', async (req, res, next) => {
|
|
try {
|
|
const { Rental, User } = require('../models');
|
|
|
|
const reviews = await Rental.findAll({
|
|
where: {
|
|
itemId: req.params.id,
|
|
status: 'completed',
|
|
itemRating: { [Op.not]: null },
|
|
itemReview: { [Op.not]: null },
|
|
itemReviewVisible: true
|
|
},
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'renter',
|
|
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
|
}
|
|
],
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
|
|
const averageRating = reviews.length > 0
|
|
? reviews.reduce((sum, review) => sum + review.itemRating, 0) / reviews.length
|
|
: 0;
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Item reviews fetched", {
|
|
itemId: req.params.id,
|
|
reviewsCount: reviews.length,
|
|
averageRating
|
|
});
|
|
|
|
res.json({
|
|
reviews,
|
|
averageRating,
|
|
totalReviews: reviews.length
|
|
});
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Item reviews fetch failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
itemId: req.params.id
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.get("/:id", optionalAuth, async (req, res, next) => {
|
|
try {
|
|
const item = await Item.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: "owner",
|
|
attributes: ["id", "firstName", "lastName", "imageFilename"],
|
|
},
|
|
{
|
|
model: User,
|
|
as: "deleter",
|
|
attributes: ["id", "firstName", "lastName"],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!item) {
|
|
return res.status(404).json({ error: "Item not found" });
|
|
}
|
|
|
|
// Check if item is deleted - only allow admins to view
|
|
if (item.isDeleted) {
|
|
const isAdmin = req.user?.role === 'admin';
|
|
|
|
if (!isAdmin) {
|
|
return res.status(404).json({ error: "Item not found" });
|
|
}
|
|
}
|
|
|
|
// Round coordinates to 2 decimal places for map display while keeping precise values in database
|
|
const itemResponse = item.toJSON();
|
|
if (itemResponse.latitude !== null && itemResponse.latitude !== undefined) {
|
|
itemResponse.latitude = Math.round(parseFloat(itemResponse.latitude) * 100) / 100;
|
|
}
|
|
if (itemResponse.longitude !== null && itemResponse.longitude !== undefined) {
|
|
itemResponse.longitude = Math.round(parseFloat(itemResponse.longitude) * 100) / 100;
|
|
}
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Item fetched", {
|
|
itemId: req.params.id,
|
|
ownerId: item.ownerId
|
|
});
|
|
|
|
res.json(itemResponse);
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Item fetch failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
itemId: req.params.id
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post("/", authenticateToken, requireVerifiedEmail, ...validateCoordinatesBody, handleValidationErrors, async (req, res, next) => {
|
|
try {
|
|
// Extract only allowed fields (prevents mass assignment)
|
|
const allowedData = extractAllowedFields(req.body);
|
|
|
|
// Validate imageFilenames - at least one image is required
|
|
const imageFilenames = Array.isArray(allowedData.imageFilenames)
|
|
? allowedData.imageFilenames
|
|
: [];
|
|
|
|
if (imageFilenames.length === 0) {
|
|
return res.status(400).json({
|
|
error: "At least one image is required to create a listing"
|
|
});
|
|
}
|
|
|
|
// Validate required fields
|
|
if (!allowedData.name || !allowedData.name.trim()) {
|
|
return res.status(400).json({ error: "Item name is required" });
|
|
}
|
|
if (!allowedData.address1 || !allowedData.address1.trim()) {
|
|
return res.status(400).json({ error: "Address is required" });
|
|
}
|
|
if (!allowedData.city || !allowedData.city.trim()) {
|
|
return res.status(400).json({ error: "City is required" });
|
|
}
|
|
if (!allowedData.state || !allowedData.state.trim()) {
|
|
return res.status(400).json({ error: "State is required" });
|
|
}
|
|
if (!allowedData.zipCode || !allowedData.zipCode.trim()) {
|
|
return res.status(400).json({ error: "ZIP code is required" });
|
|
}
|
|
if (!allowedData.replacementCost || Number(allowedData.replacementCost) <= 0) {
|
|
return res.status(400).json({ error: "Replacement cost is required" });
|
|
}
|
|
|
|
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({
|
|
...allowedData,
|
|
ownerId: req.user.id,
|
|
});
|
|
|
|
const itemWithOwner = await Item.findByPk(item.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: "owner",
|
|
attributes: ["id", "firstName", "lastName", "email", "stripeConnectedAccountId"],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Check if this is the owner's first listing
|
|
const ownerItemCount = await Item.count({
|
|
where: { ownerId: req.user.id }
|
|
});
|
|
|
|
// If first listing, send celebration email
|
|
if (ownerItemCount === 1) {
|
|
try {
|
|
const emailServices = require("../services/email");
|
|
await emailServices.userEngagement.sendFirstListingCelebrationEmail(
|
|
itemWithOwner.owner,
|
|
itemWithOwner
|
|
);
|
|
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
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Item created", {
|
|
itemId: item.id,
|
|
ownerId: req.user.id,
|
|
itemName: req.body.name,
|
|
isFirstListing: ownerItemCount === 1
|
|
});
|
|
|
|
res.status(201).json(itemWithOwner);
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Item creation failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
ownerId: req.user.id,
|
|
itemData: logger.sanitize(req.body)
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.put("/:id", authenticateToken, ...validateCoordinatesBody, handleValidationErrors, async (req, res, next) => {
|
|
try {
|
|
const item = await Item.findByPk(req.params.id);
|
|
|
|
if (!item) {
|
|
return res.status(404).json({ error: "Item not found" });
|
|
}
|
|
|
|
if (item.ownerId !== req.user.id) {
|
|
return res.status(403).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
// 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
|
|
: [];
|
|
|
|
// Require at least one image
|
|
if (imageFilenames.length === 0) {
|
|
return res.status(400).json({
|
|
error: "At least one image is required for a listing"
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Validate required fields if they are being updated
|
|
if (allowedData.name !== undefined && (!allowedData.name || !allowedData.name.trim())) {
|
|
return res.status(400).json({ error: "Item name is required" });
|
|
}
|
|
if (allowedData.address1 !== undefined && (!allowedData.address1 || !allowedData.address1.trim())) {
|
|
return res.status(400).json({ error: "Address is required" });
|
|
}
|
|
if (allowedData.city !== undefined && (!allowedData.city || !allowedData.city.trim())) {
|
|
return res.status(400).json({ error: "City is required" });
|
|
}
|
|
if (allowedData.state !== undefined && (!allowedData.state || !allowedData.state.trim())) {
|
|
return res.status(400).json({ error: "State is required" });
|
|
}
|
|
if (allowedData.zipCode !== undefined && (!allowedData.zipCode || !allowedData.zipCode.trim())) {
|
|
return res.status(400).json({ error: "ZIP code is required" });
|
|
}
|
|
if (allowedData.replacementCost !== undefined && (!allowedData.replacementCost || Number(allowedData.replacementCost) <= 0)) {
|
|
return res.status(400).json({ error: "Replacement cost is required" });
|
|
}
|
|
|
|
await item.update(allowedData);
|
|
|
|
const updatedItem = await Item.findByPk(item.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: "owner",
|
|
attributes: ["id", "firstName", "lastName"],
|
|
},
|
|
],
|
|
});
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Item updated", {
|
|
itemId: req.params.id,
|
|
ownerId: req.user.id
|
|
});
|
|
|
|
res.json(updatedItem);
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Item update failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
itemId: req.params.id,
|
|
ownerId: req.user.id
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.delete("/:id", authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const item = await Item.findByPk(req.params.id);
|
|
|
|
if (!item) {
|
|
return res.status(404).json({ error: "Item not found" });
|
|
}
|
|
|
|
if (item.ownerId !== req.user.id) {
|
|
return res.status(403).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
await item.destroy();
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Item deleted", {
|
|
itemId: req.params.id,
|
|
ownerId: req.user.id
|
|
});
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Item deletion failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
itemId: req.params.id,
|
|
ownerId: req.user.id
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Admin endpoints
|
|
router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res, next) => {
|
|
try {
|
|
const { reason } = req.body;
|
|
|
|
if (!reason || !reason.trim()) {
|
|
return res.status(400).json({ error: "Deletion reason is required" });
|
|
}
|
|
|
|
const item = await Item.findByPk(req.params.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: "owner",
|
|
attributes: ["id", "firstName", "lastName", "email"],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!item) {
|
|
return res.status(404).json({ error: "Item not found" });
|
|
}
|
|
|
|
if (item.isDeleted) {
|
|
return res.status(400).json({ error: "Item is already deleted" });
|
|
}
|
|
|
|
// Check for active or upcoming rentals
|
|
const activeRentals = await Rental.count({
|
|
where: {
|
|
itemId: req.params.id,
|
|
status: {
|
|
[Op.in]: ['pending', 'confirmed', 'active']
|
|
}
|
|
}
|
|
});
|
|
|
|
if (activeRentals > 0) {
|
|
return res.status(400).json({
|
|
error: "Cannot delete item with active or upcoming rentals",
|
|
code: "ACTIVE_RENTALS_EXIST",
|
|
activeRentalsCount: activeRentals
|
|
});
|
|
}
|
|
|
|
// Soft delete the item
|
|
await item.update({
|
|
isDeleted: true,
|
|
deletedBy: req.user.id,
|
|
deletedAt: new Date(),
|
|
deletionReason: reason.trim()
|
|
});
|
|
|
|
const updatedItem = await Item.findByPk(item.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: "owner",
|
|
attributes: ["id", "firstName", "lastName"],
|
|
},
|
|
{
|
|
model: User,
|
|
as: "deleter",
|
|
attributes: ["id", "firstName", "lastName"],
|
|
}
|
|
],
|
|
});
|
|
|
|
// Send email notification to owner
|
|
try {
|
|
const emailServices = require("../services/email");
|
|
await emailServices.userEngagement.sendItemDeletionNotificationToOwner(
|
|
item.owner,
|
|
item,
|
|
reason.trim()
|
|
);
|
|
logger.info("Item deletion notification email sent", { ownerId: item.ownerId, itemId: item.id });
|
|
} catch (emailError) {
|
|
// Log but don't fail the deletion
|
|
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);
|
|
reqLogger.info("Item soft deleted by admin", {
|
|
itemId: req.params.id,
|
|
deletedBy: req.user.id,
|
|
ownerId: item.ownerId,
|
|
reason: reason.trim()
|
|
});
|
|
|
|
res.json(updatedItem);
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Admin item soft delete failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
itemId: req.params.id,
|
|
adminId: req.user.id
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.patch("/admin/:id/restore", authenticateToken, requireAdmin, async (req, res, next) => {
|
|
try {
|
|
const item = await Item.findByPk(req.params.id);
|
|
|
|
if (!item) {
|
|
return res.status(404).json({ error: "Item not found" });
|
|
}
|
|
|
|
if (!item.isDeleted) {
|
|
return res.status(400).json({ error: "Item is not deleted" });
|
|
}
|
|
|
|
// Restore the item
|
|
await item.update({
|
|
isDeleted: false,
|
|
deletedBy: null,
|
|
deletedAt: null,
|
|
deletionReason: null
|
|
});
|
|
|
|
const updatedItem = await Item.findByPk(item.id, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: "owner",
|
|
attributes: ["id", "firstName", "lastName"],
|
|
}
|
|
],
|
|
});
|
|
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.info("Item restored by admin", {
|
|
itemId: req.params.id,
|
|
restoredBy: req.user.id,
|
|
ownerId: item.ownerId
|
|
});
|
|
|
|
res.json(updatedItem);
|
|
} catch (error) {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.error("Admin item restore failed", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
itemId: req.params.id,
|
|
adminId: req.user.id
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|