email verification flow updated
This commit is contained in:
@@ -20,7 +20,9 @@ const {
|
||||
loginLimiter,
|
||||
registerLimiter,
|
||||
passwordResetLimiter,
|
||||
emailVerificationLimiter,
|
||||
} = require("../middleware/rateLimiter");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const router = express.Router();
|
||||
|
||||
const googleClient = new OAuth2Client(
|
||||
@@ -43,8 +45,7 @@ router.post(
|
||||
validateRegistration,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { email, password, firstName, lastName, phone } =
|
||||
req.body;
|
||||
const { email, password, firstName, lastName, phone } = req.body;
|
||||
|
||||
const existingUser = await User.findOne({
|
||||
where: { email },
|
||||
@@ -64,7 +65,7 @@ router.post(
|
||||
|
||||
// Alpha access validation
|
||||
let alphaInvitation = null;
|
||||
if (process.env.ALPHA_TESTING_ENABLED === 'true') {
|
||||
if (process.env.ALPHA_TESTING_ENABLED === "true") {
|
||||
if (req.cookies && req.cookies.alphaAccessCode) {
|
||||
const { code } = req.cookies.alphaAccessCode;
|
||||
if (code) {
|
||||
@@ -88,7 +89,8 @@ router.post(
|
||||
|
||||
if (!alphaInvitation) {
|
||||
return res.status(403).json({
|
||||
error: "Alpha access required. Please enter your invitation code first.",
|
||||
error:
|
||||
"Alpha access required. Please enter your invitation code first.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -116,7 +118,10 @@ router.post(
|
||||
// Send verification email (don't block registration if email fails)
|
||||
let verificationEmailSent = false;
|
||||
try {
|
||||
await emailServices.auth.sendVerificationEmail(user, user.verificationToken);
|
||||
await emailServices.auth.sendVerificationEmail(
|
||||
user,
|
||||
user.verificationToken
|
||||
);
|
||||
verificationEmailSent = true;
|
||||
} catch (emailError) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
@@ -200,7 +205,10 @@ router.post(
|
||||
const user = await User.findOne({ where: { email } });
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
return res.status(401).json({
|
||||
error:
|
||||
"Unable to log in. Please check your email and password, or create an account.",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if account is locked
|
||||
@@ -217,7 +225,10 @@ router.post(
|
||||
if (!isPasswordValid) {
|
||||
// Increment login attempts
|
||||
await user.incLoginAttempts();
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
return res.status(401).json({
|
||||
error:
|
||||
"Unable to log in. Please check your email and password, or create an account.",
|
||||
});
|
||||
}
|
||||
|
||||
// Reset login attempts on successful login
|
||||
@@ -322,7 +333,8 @@ router.post(
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
error: "Email permission is required to continue. Please grant email access when signing in with Google and try again."
|
||||
error:
|
||||
"Email permission is required to continue. Please grant email access when signing in with Google and try again.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,18 +344,22 @@ router.post(
|
||||
let lastName = familyName;
|
||||
|
||||
if (!firstName || !lastName) {
|
||||
const emailUsername = email.split('@')[0];
|
||||
const emailUsername = email.split("@")[0];
|
||||
// Try to split email username by common separators
|
||||
const nameParts = emailUsername.split(/[._-]/);
|
||||
|
||||
if (!firstName) {
|
||||
firstName = nameParts[0] ? nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1) : 'Google';
|
||||
firstName = nameParts[0]
|
||||
? nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1)
|
||||
: "Google";
|
||||
}
|
||||
|
||||
if (!lastName) {
|
||||
lastName = nameParts.length > 1
|
||||
? nameParts[nameParts.length - 1].charAt(0).toUpperCase() + nameParts[nameParts.length - 1].slice(1)
|
||||
: 'User';
|
||||
lastName =
|
||||
nameParts.length > 1
|
||||
? nameParts[nameParts.length - 1].charAt(0).toUpperCase() +
|
||||
nameParts[nameParts.length - 1].slice(1)
|
||||
: "User";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +391,7 @@ router.post(
|
||||
});
|
||||
|
||||
// Check if there's an alpha invitation for this email
|
||||
if (process.env.ALPHA_TESTING_ENABLED === 'true') {
|
||||
if (process.env.ALPHA_TESTING_ENABLED === "true") {
|
||||
const alphaInvitation = await AlphaInvitation.findOne({
|
||||
where: { email: email.toLowerCase().trim() },
|
||||
});
|
||||
@@ -467,73 +483,121 @@ router.post(
|
||||
);
|
||||
|
||||
// Email verification endpoint
|
||||
router.post("/verify-email", sanitizeInput, async (req, res) => {
|
||||
try {
|
||||
const { token } = req.body;
|
||||
router.post(
|
||||
"/verify-email",
|
||||
emailVerificationLimiter,
|
||||
authenticateToken,
|
||||
sanitizeInput,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { code } = req.body;
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).json({
|
||||
error: "Verification token required",
|
||||
code: "TOKEN_REQUIRED",
|
||||
});
|
||||
}
|
||||
if (!code) {
|
||||
return res.status(400).json({
|
||||
error: "Verification code required",
|
||||
code: "CODE_REQUIRED",
|
||||
});
|
||||
}
|
||||
|
||||
// Find user with this verification token
|
||||
const user = await User.findOne({
|
||||
where: { verificationToken: token },
|
||||
});
|
||||
// Validate 6-digit format
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
return res.status(400).json({
|
||||
error: "Verification code must be 6 digits",
|
||||
code: "INVALID_CODE_FORMAT",
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid verification token",
|
||||
code: "VERIFICATION_TOKEN_INVALID",
|
||||
});
|
||||
}
|
||||
// Get the authenticated user
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
// Check if already verified
|
||||
if (user.isVerified) {
|
||||
return res.status(400).json({
|
||||
error: "Email already verified",
|
||||
code: "ALREADY_VERIFIED",
|
||||
});
|
||||
}
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
error: "User not found",
|
||||
code: "USER_NOT_FOUND",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if token is valid (not expired)
|
||||
if (!user.isVerificationTokenValid(token)) {
|
||||
return res.status(400).json({
|
||||
error: "Verification token has expired. Please request a new one.",
|
||||
code: "VERIFICATION_TOKEN_EXPIRED",
|
||||
});
|
||||
}
|
||||
// Check if already verified
|
||||
if (user.isVerified) {
|
||||
return res.status(400).json({
|
||||
error: "Email already verified",
|
||||
code: "ALREADY_VERIFIED",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the email
|
||||
await user.verifyEmail();
|
||||
// Check if too many failed attempts
|
||||
if (user.isVerificationLocked()) {
|
||||
return res.status(429).json({
|
||||
error: "Too many verification attempts. Please request a new code.",
|
||||
code: "TOO_MANY_ATTEMPTS",
|
||||
});
|
||||
}
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Email verified successfully", {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
// Check if user has a verification token
|
||||
if (!user.verificationToken) {
|
||||
return res.status(400).json({
|
||||
error: "No verification code found. Please request a new one.",
|
||||
code: "NO_CODE",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: "Email verified successfully",
|
||||
user: {
|
||||
id: user.id,
|
||||
// Check if code is expired
|
||||
if (
|
||||
user.verificationTokenExpiry &&
|
||||
new Date() > new Date(user.verificationTokenExpiry)
|
||||
) {
|
||||
return res.status(400).json({
|
||||
error: "Verification code has expired. Please request a new one.",
|
||||
code: "VERIFICATION_EXPIRED",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate the code
|
||||
if (!user.isVerificationTokenValid(input)) {
|
||||
// Increment failed attempts
|
||||
await user.incrementVerificationAttempts();
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.warn("Invalid verification code attempt", {
|
||||
userId: user.id,
|
||||
attempts: user.verificationAttempts + 1,
|
||||
});
|
||||
|
||||
return res.status(400).json({
|
||||
error: "Invalid verification code",
|
||||
code: "VERIFICATION_INVALID",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the email
|
||||
await user.verifyEmail();
|
||||
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Email verified successfully", {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Email verification error", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
res.status(500).json({
|
||||
error: "Email verification failed. Please try again.",
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: "Email verified successfully",
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Email verification error", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
res.status(500).json({
|
||||
error: "Email verification failed. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// Resend verification email endpoint
|
||||
router.post(
|
||||
@@ -575,7 +639,10 @@ router.post(
|
||||
|
||||
// Send verification email
|
||||
try {
|
||||
await emailServices.auth.sendVerificationEmail(user, user.verificationToken);
|
||||
await emailServices.auth.sendVerificationEmail(
|
||||
user,
|
||||
user.verificationToken
|
||||
);
|
||||
} catch (emailError) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Failed to resend verification email", {
|
||||
|
||||
@@ -241,6 +241,14 @@ 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 {
|
||||
// Require email verification
|
||||
if (!req.user.isVerified) {
|
||||
return res.status(403).json({
|
||||
error: "Please verify your email address before creating forum posts.",
|
||||
code: "EMAIL_NOT_VERIFIED"
|
||||
});
|
||||
}
|
||||
|
||||
let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng, imageFilenames: rawImageFilenames } = req.body;
|
||||
|
||||
// Ensure imageFilenames is an array and validate S3 keys
|
||||
@@ -490,6 +498,14 @@ router.post('/posts', authenticateToken, async (req, res, next) => {
|
||||
// PUT /api/forum/posts/:id - Update post
|
||||
router.put('/posts/:id', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
// Require email verification
|
||||
if (!req.user.isVerified) {
|
||||
return res.status(403).json({
|
||||
error: "Please verify your email address before editing forum posts.",
|
||||
code: "EMAIL_NOT_VERIFIED"
|
||||
});
|
||||
}
|
||||
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
if (!post) {
|
||||
@@ -934,6 +950,14 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res, nex
|
||||
// POST /api/forum/posts/:id/comments - Add comment/reply
|
||||
router.post('/posts/:id/comments', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
// Require email verification
|
||||
if (!req.user.isVerified) {
|
||||
return res.status(403).json({
|
||||
error: "Please verify your email address before commenting.",
|
||||
code: "EMAIL_NOT_VERIFIED"
|
||||
});
|
||||
}
|
||||
|
||||
// Support both parentId (new) and parentCommentId (legacy) for backwards compatibility
|
||||
const { content, parentId, parentCommentId, imageFilenames: rawImageFilenames } = req.body;
|
||||
const parentIdResolved = parentId || parentCommentId;
|
||||
@@ -1111,6 +1135,14 @@ router.post('/posts/:id/comments', authenticateToken, async (req, res, next) =>
|
||||
// PUT /api/forum/comments/:id - Edit comment
|
||||
router.put('/comments/:id', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
// Require email verification
|
||||
if (!req.user.isVerified) {
|
||||
return res.status(403).json({
|
||||
error: "Please verify your email address before editing comments.",
|
||||
code: "EMAIL_NOT_VERIFIED"
|
||||
});
|
||||
}
|
||||
|
||||
const { content, imageFilenames: rawImageFilenames } = req.body;
|
||||
const comment = await ForumComment.findByPk(req.params.id);
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
const express = require("express");
|
||||
const { Op } = require("sequelize");
|
||||
const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations
|
||||
const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth");
|
||||
const {
|
||||
authenticateToken,
|
||||
requireVerifiedEmail,
|
||||
} = require("../middleware/auth");
|
||||
const FeeCalculator = require("../utils/feeCalculator");
|
||||
const RentalDurationCalculator = require("../utils/rentalDurationCalculator");
|
||||
const RefundService = require("../services/refundService");
|
||||
@@ -304,7 +307,11 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
||||
|
||||
// Send rental request notification to owner
|
||||
try {
|
||||
await emailServices.rentalFlow.sendRentalRequestEmail(rentalWithDetails.owner, rentalWithDetails.renter, rentalWithDetails);
|
||||
await emailServices.rentalFlow.sendRentalRequestEmail(
|
||||
rentalWithDetails.owner,
|
||||
rentalWithDetails.renter,
|
||||
rentalWithDetails
|
||||
);
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Rental request notification sent to owner", {
|
||||
rentalId: rental.id,
|
||||
@@ -322,7 +329,10 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
||||
|
||||
// Send rental request confirmation to renter
|
||||
try {
|
||||
await emailServices.rentalFlow.sendRentalRequestConfirmationEmail(rentalWithDetails.renter, rentalWithDetails);
|
||||
await emailServices.rentalFlow.sendRentalRequestConfirmationEmail(
|
||||
rentalWithDetails.renter,
|
||||
rentalWithDetails
|
||||
);
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Rental request confirmation sent to renter", {
|
||||
rentalId: rental.id,
|
||||
@@ -358,12 +368,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
{
|
||||
model: User,
|
||||
as: "renter",
|
||||
attributes: [
|
||||
"id",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"stripeCustomerId",
|
||||
],
|
||||
attributes: ["id", "firstName", "lastName", "stripeCustomerId"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -445,7 +450,11 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
// Send confirmation emails
|
||||
// Send approval confirmation to owner with Stripe reminder
|
||||
try {
|
||||
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(updatedRental.owner, updatedRental.renter, updatedRental);
|
||||
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(
|
||||
updatedRental.owner,
|
||||
updatedRental.renter,
|
||||
updatedRental
|
||||
);
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Rental approval confirmation sent to owner", {
|
||||
rentalId: updatedRental.id,
|
||||
@@ -453,11 +462,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
});
|
||||
} catch (emailError) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Failed to send rental approval confirmation email to owner", {
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
ownerId: updatedRental.ownerId,
|
||||
});
|
||||
reqLogger.error(
|
||||
"Failed to send rental approval confirmation email to owner",
|
||||
{
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
ownerId: updatedRental.ownerId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Send rental confirmation to renter with payment receipt
|
||||
@@ -489,11 +501,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
}
|
||||
} catch (emailError) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Failed to send rental confirmation email to renter", {
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
renterId: updatedRental.renterId,
|
||||
});
|
||||
reqLogger.error(
|
||||
"Failed to send rental confirmation email to renter",
|
||||
{
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
renterId: updatedRental.renterId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
res.json(updatedRental);
|
||||
@@ -537,7 +552,11 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
// Send confirmation emails
|
||||
// Send approval confirmation to owner (for free rentals, no Stripe reminder shown)
|
||||
try {
|
||||
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(updatedRental.owner, updatedRental.renter, updatedRental);
|
||||
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(
|
||||
updatedRental.owner,
|
||||
updatedRental.renter,
|
||||
updatedRental
|
||||
);
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Rental approval confirmation sent to owner", {
|
||||
rentalId: updatedRental.id,
|
||||
@@ -545,11 +564,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
});
|
||||
} catch (emailError) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Failed to send rental approval confirmation email to owner", {
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
ownerId: updatedRental.ownerId,
|
||||
});
|
||||
reqLogger.error(
|
||||
"Failed to send rental approval confirmation email to owner",
|
||||
{
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
ownerId: updatedRental.ownerId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Send rental confirmation to renter
|
||||
@@ -581,11 +603,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
}
|
||||
} catch (emailError) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Failed to send rental confirmation email to renter", {
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
renterId: updatedRental.renterId,
|
||||
});
|
||||
reqLogger.error(
|
||||
"Failed to send rental confirmation email to renter",
|
||||
{
|
||||
error: emailError.message,
|
||||
rentalId: updatedRental.id,
|
||||
renterId: updatedRental.renterId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
res.json(updatedRental);
|
||||
@@ -687,7 +712,11 @@ router.put("/:id/decline", authenticateToken, async (req, res) => {
|
||||
|
||||
// Send decline notification email to renter
|
||||
try {
|
||||
await emailServices.rentalFlow.sendRentalDeclinedEmail(updatedRental.renter, updatedRental, reason);
|
||||
await emailServices.rentalFlow.sendRentalDeclinedEmail(
|
||||
updatedRental.renter,
|
||||
updatedRental,
|
||||
reason
|
||||
);
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Rental decline notification sent to renter", {
|
||||
rentalId: rental.id,
|
||||
@@ -904,7 +933,7 @@ router.post("/cost-preview", authenticateToken, async (req, res) => {
|
||||
// Validate date range
|
||||
if (rentalEndDateTime <= rentalStartDateTime) {
|
||||
return res.status(400).json({
|
||||
error: "End date/time must be after start date/time",
|
||||
error: "End must be after start",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -987,44 +1016,50 @@ router.get("/:id/refund-preview", authenticateToken, async (req, res, next) => {
|
||||
});
|
||||
|
||||
// Get late fee preview
|
||||
router.get("/:id/late-fee-preview", authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const { actualReturnDateTime } = req.query;
|
||||
router.get(
|
||||
"/:id/late-fee-preview",
|
||||
authenticateToken,
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { actualReturnDateTime } = req.query;
|
||||
|
||||
if (!actualReturnDateTime) {
|
||||
return res.status(400).json({ error: "actualReturnDateTime is required" });
|
||||
if (!actualReturnDateTime) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "actualReturnDateTime is required" });
|
||||
}
|
||||
|
||||
const rental = await Rental.findByPk(req.params.id, {
|
||||
include: [{ model: Item, as: "item" }],
|
||||
});
|
||||
|
||||
if (!rental) {
|
||||
return res.status(404).json({ error: "Rental not found" });
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) {
|
||||
return res.status(403).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const lateCalculation = LateReturnService.calculateLateFee(
|
||||
rental,
|
||||
actualReturnDateTime
|
||||
);
|
||||
|
||||
res.json(lateCalculation);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Error getting late fee preview", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.id,
|
||||
});
|
||||
next(error);
|
||||
}
|
||||
|
||||
const rental = await Rental.findByPk(req.params.id, {
|
||||
include: [{ model: Item, as: "item" }],
|
||||
});
|
||||
|
||||
if (!rental) {
|
||||
return res.status(404).json({ error: "Rental not found" });
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) {
|
||||
return res.status(403).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const lateCalculation = LateReturnService.calculateLateFee(
|
||||
rental,
|
||||
actualReturnDateTime
|
||||
);
|
||||
|
||||
res.json(lateCalculation);
|
||||
} catch (error) {
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.error("Error getting late fee preview", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.id,
|
||||
});
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// Cancel rental with refund processing
|
||||
router.post("/:id/cancel", authenticateToken, async (req, res, next) => {
|
||||
@@ -1156,7 +1191,11 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => {
|
||||
|
||||
// Send completion emails to both renter and owner
|
||||
try {
|
||||
await emailServices.rentalFlow.sendRentalCompletionEmails(rentalWithDetails.owner, rentalWithDetails.renter, rentalWithDetails);
|
||||
await emailServices.rentalFlow.sendRentalCompletionEmails(
|
||||
rentalWithDetails.owner,
|
||||
rentalWithDetails.renter,
|
||||
rentalWithDetails
|
||||
);
|
||||
const reqLogger = logger.withRequestId(req.id);
|
||||
reqLogger.info("Rental completion emails sent", {
|
||||
rentalId,
|
||||
@@ -1224,7 +1263,11 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => {
|
||||
// Send notification to customer service
|
||||
const owner = await User.findByPk(rental.ownerId);
|
||||
const renter = await User.findByPk(rental.renterId);
|
||||
await emailServices.customerService.sendLostItemToCustomerService(updatedRental, owner, renter);
|
||||
await emailServices.customerService.sendLostItemToCustomerService(
|
||||
updatedRental,
|
||||
owner,
|
||||
renter
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1261,14 +1304,14 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => {
|
||||
|
||||
// Allowed fields for damage report (prevents mass assignment)
|
||||
const ALLOWED_DAMAGE_REPORT_FIELDS = [
|
||||
'description',
|
||||
'canBeFixed',
|
||||
'repairCost',
|
||||
'needsReplacement',
|
||||
'replacementCost',
|
||||
'proofOfOwnership',
|
||||
'actualReturnDateTime',
|
||||
'imageFilenames',
|
||||
"description",
|
||||
"canBeFixed",
|
||||
"repairCost",
|
||||
"needsReplacement",
|
||||
"replacementCost",
|
||||
"proofOfOwnership",
|
||||
"actualReturnDateTime",
|
||||
"imageFilenames",
|
||||
];
|
||||
|
||||
function extractAllowedDamageFields(body) {
|
||||
@@ -1295,9 +1338,13 @@ router.post("/:id/report-damage", authenticateToken, async (req, res, next) => {
|
||||
? damageInfo.imageFilenames
|
||||
: [];
|
||||
|
||||
const keyValidation = validateS3Keys(imageFilenamesArray, 'damage-reports', {
|
||||
maxKeys: IMAGE_LIMITS.damageReports,
|
||||
});
|
||||
const keyValidation = validateS3Keys(
|
||||
imageFilenamesArray,
|
||||
"damage-reports",
|
||||
{
|
||||
maxKeys: IMAGE_LIMITS.damageReports,
|
||||
}
|
||||
);
|
||||
if (!keyValidation.valid) {
|
||||
return res.status(400).json({
|
||||
error: keyValidation.error,
|
||||
|
||||
Reference in New Issue
Block a user