feedback tab
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
34
backend/models/Feedback.js
Normal file
34
backend/models/Feedback.js
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
66
backend/routes/feedback.js
Normal file
66
backend/routes/feedback.js
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
117
backend/templates/emails/feedbackConfirmationToUser.html
Normal file
117
backend/templates/emails/feedbackConfirmationToUser.html
Normal 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>© {{year}} RentAll. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
159
backend/templates/emails/feedbackNotificationToAdmin.html
Normal file
159
backend/templates/emails/feedbackNotificationToAdmin.html
Normal 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>© {{year}} RentAll. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user