feedback tab

This commit is contained in:
jackiettran
2025-10-31 16:48:18 -04:00
parent 99aa0b3bdc
commit 16272ba373
13 changed files with 812 additions and 1 deletions

View File

@@ -310,6 +310,22 @@ const validateVerifyResetToken = [
handleValidationErrors,
];
// Feedback validation
const validateFeedback = [
body("feedbackText")
.trim()
.isLength({ min: 5, max: 5000 })
.withMessage("Feedback must be between 5 and 5000 characters"),
body("url")
.optional()
.trim()
.isLength({ max: 500 })
.withMessage("URL must be less than 500 characters"),
handleValidationErrors,
];
module.exports = {
sanitizeInput,
handleValidationErrors,
@@ -321,4 +337,5 @@ module.exports = {
validateForgotPassword,
validateResetPassword,
validateVerifyResetToken,
validateFeedback,
};

View File

@@ -0,0 +1,34 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const Feedback = sequelize.define('Feedback', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'Users',
key: 'id'
}
},
feedbackText: {
type: DataTypes.TEXT,
allowNull: false
},
userAgent: {
type: DataTypes.STRING,
allowNull: true
},
url: {
type: DataTypes.STRING(500),
allowNull: true
}
}, {
timestamps: true
});
module.exports = Feedback;

View File

@@ -8,6 +8,7 @@ const ItemRequestResponse = require("./ItemRequestResponse");
const UserAddress = require("./UserAddress");
const ConditionCheck = require("./ConditionCheck");
const AlphaInvitation = require("./AlphaInvitation");
const Feedback = require("./Feedback");
User.hasMany(Item, { as: "ownedItems", foreignKey: "ownerId" });
Item.belongsTo(User, { as: "owner", foreignKey: "ownerId" });
@@ -82,6 +83,10 @@ User.hasMany(AlphaInvitation, {
foreignKey: "usedBy",
});
// Feedback associations
User.hasMany(Feedback, { as: "feedbacks", foreignKey: "userId" });
Feedback.belongsTo(User, { as: "user", foreignKey: "userId" });
module.exports = {
sequelize,
User,
@@ -93,4 +98,5 @@ module.exports = {
UserAddress,
ConditionCheck,
AlphaInvitation,
Feedback,
};

View File

@@ -0,0 +1,66 @@
const express = require('express');
const { Feedback, User } = require('../models');
const { authenticateToken } = require('../middleware/auth');
const { validateFeedback, sanitizeInput } = require('../middleware/validation');
const logger = require('../utils/logger');
const emailService = require('../services/emailService');
const router = express.Router();
// Submit new feedback
router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, res) => {
try {
const { feedbackText, url } = req.body;
// Capture user agent from request headers
const userAgent = req.headers['user-agent'];
const feedback = await Feedback.create({
userId: req.user.id,
feedbackText,
url: url || null,
userAgent: userAgent || null
});
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Feedback submitted", {
userId: req.user.id,
feedbackId: feedback.id
});
// Send confirmation email to user
try {
await emailService.sendFeedbackConfirmation(req.user, feedback);
} catch (emailError) {
reqLogger.error("Failed to send feedback confirmation email", {
error: emailError.message,
userId: req.user.id,
feedbackId: feedback.id
});
// Don't fail the request if email fails
}
// Send notification email to admin
try {
await emailService.sendFeedbackNotificationToAdmin(req.user, feedback);
} catch (emailError) {
reqLogger.error("Failed to send feedback notification to admin", {
error: emailError.message,
userId: req.user.id,
feedbackId: feedback.id
});
// Don't fail the request if email fails
}
res.status(201).json(feedback);
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Feedback submission failed", {
error: error.message,
stack: error.stack,
userId: req.user.id
});
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -25,6 +25,7 @@ const itemRequestRoutes = require("./routes/itemRequests");
const stripeRoutes = require("./routes/stripe");
const mapsRoutes = require("./routes/maps");
const conditionCheckRoutes = require("./routes/conditionChecks");
const feedbackRoutes = require("./routes/feedback");
const PayoutProcessor = require("./jobs/payoutProcessor");
const RentalStatusJob = require("./jobs/rentalStatusJob");
@@ -126,6 +127,7 @@ app.use("/api/item-requests", requireAlphaAccess, itemRequestRoutes);
app.use("/api/stripe", requireAlphaAccess, stripeRoutes);
app.use("/api/maps", requireAlphaAccess, mapsRoutes);
app.use("/api/condition-checks", requireAlphaAccess, conditionCheckRoutes);
app.use("/api/feedback", requireAlphaAccess, feedbackRoutes);
// Error handling middleware (must be last)
app.use(errorLogger);

View File

@@ -52,6 +52,8 @@ class EmailService {
"payoutReceivedToOwner.html",
"firstListingCelebrationToOwner.html",
"alphaInvitationToUser.html",
"feedbackConfirmationToUser.html",
"feedbackNotificationToAdmin.html",
];
for (const templateFile of templateFiles) {
@@ -443,6 +445,40 @@ class EmailService {
<p><a href="{{myListingsUrl}}" class="button">View My Listings</a></p>
`
),
feedbackConfirmationToUser: baseTemplate.replace(
"{{content}}",
`
<p>Hi {{userName}},</p>
<h2>Thank You for Your Feedback!</h2>
<p>We've received your feedback and our team will review it carefully.</p>
<div style="background-color: #f8f9fa; border-left: 4px solid #007bff; padding: 15px; margin: 20px 0; font-style: italic;">
{{feedbackText}}
</div>
<p><strong>Submitted:</strong> {{submittedAt}}</p>
<p>Your input helps us improve RentAll for everyone. We take all feedback seriously and use it to make the platform better.</p>
<p>If your feedback requires a response, our team will reach out to you directly.</p>
`
),
feedbackNotificationToAdmin: baseTemplate.replace(
"{{content}}",
`
<h2>New Feedback Received</h2>
<p><strong>From:</strong> {{userName}} ({{userEmail}})</p>
<p><strong>User ID:</strong> {{userId}}</p>
<p><strong>Submitted:</strong> {{submittedAt}}</p>
<h3>Feedback Content</h3>
<div style="background-color: #e7f3ff; border-left: 4px solid #007bff; padding: 20px; margin: 20px 0;">
{{feedbackText}}
</div>
<h3>Technical Context</h3>
<p><strong>Feedback ID:</strong> {{feedbackId}}</p>
<p><strong>Page URL:</strong> {{url}}</p>
<p><strong>User Agent:</strong> {{userAgent}}</p>
<p>Please review this feedback and take appropriate action if needed.</p>
`
),
};
return (
@@ -1750,6 +1786,69 @@ class EmailService {
return results;
}
async sendFeedbackConfirmation(user, feedback) {
const submittedAt = new Date(feedback.createdAt).toLocaleString("en-US", {
dateStyle: "long",
timeStyle: "short",
});
const variables = {
userName: user.firstName || "there",
userEmail: user.email,
feedbackText: feedback.feedbackText,
submittedAt: submittedAt,
year: new Date().getFullYear(),
};
const htmlContent = this.renderTemplate(
"feedbackConfirmationToUser",
variables
);
return await this.sendEmail(
user.email,
"Thank You for Your Feedback - RentAll",
htmlContent
);
}
async sendFeedbackNotificationToAdmin(user, feedback) {
const adminEmail = process.env.FEEDBACK_EMAIL || process.env.CUSTOMER_SUPPORT_EMAIL;
if (!adminEmail) {
console.warn("No admin email configured for feedback notifications");
return { success: false, error: "No admin email configured" };
}
const submittedAt = new Date(feedback.createdAt).toLocaleString("en-US", {
dateStyle: "long",
timeStyle: "short",
});
const variables = {
userName: `${user.firstName} ${user.lastName}`.trim() || "Unknown User",
userEmail: user.email,
userId: user.id,
feedbackText: feedback.feedbackText,
feedbackId: feedback.id,
url: feedback.url || "Not provided",
userAgent: feedback.userAgent || "Not provided",
submittedAt: submittedAt,
year: new Date().getFullYear(),
};
const htmlContent = this.renderTemplate(
"feedbackNotificationToAdmin",
variables
);
return await this.sendEmail(
adminEmail,
`New Feedback from ${user.firstName} ${user.lastName}`,
htmlContent
);
}
}
module.exports = new EmailService();

View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feedback Received - RentAll</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
border-bottom: 2px solid #e9ecef;
padding-bottom: 20px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #333;
}
.content {
line-height: 1.6;
color: #555;
}
.success-box {
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.success-box .icon {
font-size: 32px;
text-align: center;
margin-bottom: 10px;
}
.success-box p {
margin: 5px 0;
color: #155724;
}
.feedback-preview {
background-color: #f8f9fa;
border-left: 4px solid #007bff;
padding: 15px;
margin: 20px 0;
font-style: italic;
color: #495057;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
text-align: center;
font-size: 12px;
color: #6c757d;
}
.button {
display: inline-block;
background-color: #007bff;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}
h2 {
color: #333;
margin-top: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">RentAll</div>
</div>
<div class="content">
<p>Hi {{userName}},</p>
<div class="success-box">
<div class="icon"></div>
<p><strong>Thank You for Your Feedback!</strong></p>
<p>We've received your feedback and our team will review it carefully.</p>
</div>
<h2>Your Feedback</h2>
<div class="feedback-preview">
{{feedbackText}}
</div>
<p><strong>Submitted:</strong> {{submittedAt}}</p>
<p>Your input helps us improve RentAll for everyone. We take all feedback seriously and use it to make the platform better.</p>
<p>If your feedback requires a response, our team will reach out to you directly at <strong>{{userEmail}}</strong>.</p>
<p>Want to share more thoughts? Feel free to send us additional feedback anytime through the app.</p>
</div>
<div class="footer">
<p>This email was sent from RentAll. If you have any questions, please contact support.</p>
<p>&copy; {{year}} RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Feedback Received - RentAll</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 700px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
border-bottom: 2px solid #e9ecef;
padding-bottom: 20px;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #333;
}
.alert-box {
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.alert-box p {
margin: 5px 0;
color: #856404;
}
.content {
line-height: 1.6;
color: #555;
}
.info-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.info-table th {
background-color: #f8f9fa;
padding: 12px;
text-align: left;
border: 1px solid #dee2e6;
font-weight: bold;
color: #495057;
width: 30%;
}
.info-table td {
padding: 12px;
border: 1px solid #dee2e6;
color: #212529;
word-break: break-word;
}
.feedback-box {
background-color: #e7f3ff;
border-left: 4px solid #007bff;
padding: 20px;
margin: 20px 0;
font-size: 14px;
line-height: 1.6;
color: #004085;
}
.feedback-box h3 {
margin-top: 0;
color: #004085;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
text-align: center;
font-size: 12px;
color: #6c757d;
}
h2 {
color: #333;
margin-top: 0;
}
.timestamp {
color: #6c757d;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">RentAll Admin</div>
</div>
<div class="content">
<div class="alert-box">
<p><strong>New Feedback Received</strong></p>
<p class="timestamp">Submitted at: {{submittedAt}}</p>
</div>
<h2>User Information</h2>
<table class="info-table">
<tr>
<th>Name</th>
<td>{{userName}}</td>
</tr>
<tr>
<th>Email</th>
<td>{{userEmail}}</td>
</tr>
<tr>
<th>User ID</th>
<td style="font-family: monospace; font-size: 12px;">{{userId}}</td>
</tr>
</table>
<div class="feedback-box">
<h3>Feedback Content</h3>
<p>{{feedbackText}}</p>
</div>
<h2>Technical Context</h2>
<table class="info-table">
<tr>
<th>Feedback ID</th>
<td style="font-family: monospace; font-size: 12px;">{{feedbackId}}</td>
</tr>
<tr>
<th>Page URL</th>
<td>{{url}}</td>
</tr>
<tr>
<th>User Agent</th>
<td style="font-size: 12px;">{{userAgent}}</td>
</tr>
<tr>
<th>Submitted At</th>
<td>{{submittedAt}}</td>
</tr>
</table>
<p><strong>Action Required:</strong> Please review this feedback and take appropriate action. If a response is needed, contact the user directly at <strong>{{userEmail}}</strong>.</p>
</div>
<div class="footer">
<p>This is an automated notification from RentAll Feedback System</p>
<p>&copy; {{year}} RentAll. All rights reserved.</p>
</div>
</div>
</body>
</html>