fixed cors bug, separating rental confirmation for owner and renter, removing condition checks from my-listings
This commit is contained in:
@@ -73,10 +73,7 @@ app.use(morgan("combined", { stream: logger.stream }));
|
|||||||
// API request/response logging
|
// API request/response logging
|
||||||
app.use("/api/", apiLogger);
|
app.use("/api/", apiLogger);
|
||||||
|
|
||||||
// General rate limiting for all routes
|
// CORS with security settings (must come BEFORE rate limiter to ensure headers on all responses)
|
||||||
app.use("/api/", generalLimiter);
|
|
||||||
|
|
||||||
// CORS with security settings
|
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||||
@@ -85,6 +82,9 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// General rate limiting for all routes
|
||||||
|
app.use("/api/", generalLimiter);
|
||||||
|
|
||||||
// Body parsing with size limits
|
// Body parsing with size limits
|
||||||
app.use(
|
app.use(
|
||||||
bodyParser.json({
|
bodyParser.json({
|
||||||
|
|||||||
@@ -415,6 +415,11 @@ class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendRentalConfirmationEmails(rental) {
|
async sendRentalConfirmationEmails(rental) {
|
||||||
|
const results = {
|
||||||
|
ownerEmailSent: false,
|
||||||
|
renterEmailSent: false,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get owner and renter emails
|
// Get owner and renter emails
|
||||||
const owner = await User.findByPk(rental.ownerId, {
|
const owner = await User.findByPk(rental.ownerId, {
|
||||||
@@ -444,30 +449,67 @@ class EmailService {
|
|||||||
metadata: { rentalStart: rental.startDateTime },
|
metadata: { rentalStart: rental.startDateTime },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send email to owner
|
// Send email to owner - independent error handling
|
||||||
if (owner?.email) {
|
if (owner?.email) {
|
||||||
await this.sendRentalConfirmation(
|
try {
|
||||||
owner.email,
|
const ownerResult = await this.sendRentalConfirmation(
|
||||||
ownerNotification,
|
owner.email,
|
||||||
rental
|
ownerNotification,
|
||||||
);
|
rental
|
||||||
console.log(`Rental confirmation email sent to owner: ${owner.email}`);
|
);
|
||||||
|
if (ownerResult.success) {
|
||||||
|
console.log(
|
||||||
|
`Rental confirmation email sent to owner: ${owner.email}`
|
||||||
|
);
|
||||||
|
results.ownerEmailSent = true;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`Failed to send rental confirmation email to owner (${owner.email}):`,
|
||||||
|
ownerResult.error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to send rental confirmation email to owner (${owner.email}):`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send email to renter
|
// Send email to renter - independent error handling
|
||||||
if (renter?.email) {
|
if (renter?.email) {
|
||||||
await this.sendRentalConfirmation(
|
try {
|
||||||
renter.email,
|
const renterResult = await this.sendRentalConfirmation(
|
||||||
renterNotification,
|
renter.email,
|
||||||
rental
|
renterNotification,
|
||||||
);
|
rental
|
||||||
console.log(
|
);
|
||||||
`Rental confirmation email sent to renter: ${renter.email}`
|
if (renterResult.success) {
|
||||||
);
|
console.log(
|
||||||
|
`Rental confirmation email sent to renter: ${renter.email}`
|
||||||
|
);
|
||||||
|
results.renterEmailSent = true;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`Failed to send rental confirmation email to renter (${renter.email}):`,
|
||||||
|
renterResult.error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to send rental confirmation email to renter (${renter.email}):`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending rental confirmation emails:", error);
|
console.error(
|
||||||
|
"Error fetching user data for rental confirmation emails:",
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,4 +248,172 @@ describe('EmailService', () => {
|
|||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sendRentalConfirmationEmails', () => {
|
||||||
|
const { User } = require('../../../models');
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockSend.mockResolvedValue({ MessageId: 'test-message-id' });
|
||||||
|
await emailService.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send emails to both owner and renter successfully', async () => {
|
||||||
|
const mockOwner = { email: 'owner@example.com' };
|
||||||
|
const mockRenter = { email: 'renter@example.com' };
|
||||||
|
|
||||||
|
User.findByPk
|
||||||
|
.mockResolvedValueOnce(mockOwner) // First call for owner
|
||||||
|
.mockResolvedValueOnce(mockRenter); // Second call for renter
|
||||||
|
|
||||||
|
const rental = {
|
||||||
|
id: 1,
|
||||||
|
ownerId: 10,
|
||||||
|
renterId: 20,
|
||||||
|
item: { name: 'Test Item' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-17T10:00:00Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await emailService.sendRentalConfirmationEmails(rental);
|
||||||
|
|
||||||
|
expect(results.ownerEmailSent).toBe(true);
|
||||||
|
expect(results.renterEmailSent).toBe(true);
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send renter email even if owner email fails', async () => {
|
||||||
|
const mockOwner = { email: 'owner@example.com' };
|
||||||
|
const mockRenter = { email: 'renter@example.com' };
|
||||||
|
|
||||||
|
User.findByPk
|
||||||
|
.mockResolvedValueOnce(mockOwner)
|
||||||
|
.mockResolvedValueOnce(mockRenter);
|
||||||
|
|
||||||
|
// First call (owner) fails, second call (renter) succeeds
|
||||||
|
mockSend
|
||||||
|
.mockRejectedValueOnce(new Error('SES Error for owner'))
|
||||||
|
.mockResolvedValueOnce({ MessageId: 'renter-message-id' });
|
||||||
|
|
||||||
|
const rental = {
|
||||||
|
id: 1,
|
||||||
|
ownerId: 10,
|
||||||
|
renterId: 20,
|
||||||
|
item: { name: 'Test Item' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-17T10:00:00Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await emailService.sendRentalConfirmationEmails(rental);
|
||||||
|
|
||||||
|
expect(results.ownerEmailSent).toBe(false);
|
||||||
|
expect(results.renterEmailSent).toBe(true);
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send owner email even if renter email fails', async () => {
|
||||||
|
const mockOwner = { email: 'owner@example.com' };
|
||||||
|
const mockRenter = { email: 'renter@example.com' };
|
||||||
|
|
||||||
|
User.findByPk
|
||||||
|
.mockResolvedValueOnce(mockOwner)
|
||||||
|
.mockResolvedValueOnce(mockRenter);
|
||||||
|
|
||||||
|
// First call (owner) succeeds, second call (renter) fails
|
||||||
|
mockSend
|
||||||
|
.mockResolvedValueOnce({ MessageId: 'owner-message-id' })
|
||||||
|
.mockRejectedValueOnce(new Error('SES Error for renter'));
|
||||||
|
|
||||||
|
const rental = {
|
||||||
|
id: 1,
|
||||||
|
ownerId: 10,
|
||||||
|
renterId: 20,
|
||||||
|
item: { name: 'Test Item' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-17T10:00:00Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await emailService.sendRentalConfirmationEmails(rental);
|
||||||
|
|
||||||
|
expect(results.ownerEmailSent).toBe(true);
|
||||||
|
expect(results.renterEmailSent).toBe(false);
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle both emails failing gracefully', async () => {
|
||||||
|
const mockOwner = { email: 'owner@example.com' };
|
||||||
|
const mockRenter = { email: 'renter@example.com' };
|
||||||
|
|
||||||
|
User.findByPk
|
||||||
|
.mockResolvedValueOnce(mockOwner)
|
||||||
|
.mockResolvedValueOnce(mockRenter);
|
||||||
|
|
||||||
|
// Both calls fail
|
||||||
|
mockSend
|
||||||
|
.mockRejectedValueOnce(new Error('SES Error for owner'))
|
||||||
|
.mockRejectedValueOnce(new Error('SES Error for renter'));
|
||||||
|
|
||||||
|
const rental = {
|
||||||
|
id: 1,
|
||||||
|
ownerId: 10,
|
||||||
|
renterId: 20,
|
||||||
|
item: { name: 'Test Item' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-17T10:00:00Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await emailService.sendRentalConfirmationEmails(rental);
|
||||||
|
|
||||||
|
expect(results.ownerEmailSent).toBe(false);
|
||||||
|
expect(results.renterEmailSent).toBe(false);
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing owner email', async () => {
|
||||||
|
const mockOwner = { email: null };
|
||||||
|
const mockRenter = { email: 'renter@example.com' };
|
||||||
|
|
||||||
|
User.findByPk
|
||||||
|
.mockResolvedValueOnce(mockOwner)
|
||||||
|
.mockResolvedValueOnce(mockRenter);
|
||||||
|
|
||||||
|
const rental = {
|
||||||
|
id: 1,
|
||||||
|
ownerId: 10,
|
||||||
|
renterId: 20,
|
||||||
|
item: { name: 'Test Item' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-17T10:00:00Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await emailService.sendRentalConfirmationEmails(rental);
|
||||||
|
|
||||||
|
expect(results.ownerEmailSent).toBe(false);
|
||||||
|
expect(results.renterEmailSent).toBe(true);
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing renter email', async () => {
|
||||||
|
const mockOwner = { email: 'owner@example.com' };
|
||||||
|
const mockRenter = { email: null };
|
||||||
|
|
||||||
|
User.findByPk
|
||||||
|
.mockResolvedValueOnce(mockOwner)
|
||||||
|
.mockResolvedValueOnce(mockRenter);
|
||||||
|
|
||||||
|
const rental = {
|
||||||
|
id: 1,
|
||||||
|
ownerId: 10,
|
||||||
|
renterId: 20,
|
||||||
|
item: { name: 'Test Item' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-17T10:00:00Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await emailService.sendRentalConfirmationEmails(rental);
|
||||||
|
|
||||||
|
expect(results.ownerEmailSent).toBe(true);
|
||||||
|
expect(results.renterEmailSent).toBe(false);
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
import { rentalAPI, conditionCheckAPI } from "../services/api";
|
||||||
import { Rental } from "../types";
|
import { Rental } from "../types";
|
||||||
|
|
||||||
@@ -69,6 +69,18 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
|||||||
}
|
}
|
||||||
}, [show, rental]);
|
}, [show, rental]);
|
||||||
|
|
||||||
|
// Create blob URLs for photo previews and clean them up
|
||||||
|
const photoBlobUrls = useMemo(() => {
|
||||||
|
return photos.map(photo => URL.createObjectURL(photo));
|
||||||
|
}, [photos]);
|
||||||
|
|
||||||
|
// Cleanup blob URLs when photos change or component unmounts
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
photoBlobUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
|
};
|
||||||
|
}, [photoBlobUrls]);
|
||||||
|
|
||||||
const formatCurrency = (amount: number | string | undefined) => {
|
const formatCurrency = (amount: number | string | undefined) => {
|
||||||
const numAmount = Number(amount) || 0;
|
const numAmount = Number(amount) || 0;
|
||||||
return `$${numAmount.toFixed(2)}`;
|
return `$${numAmount.toFixed(2)}`;
|
||||||
@@ -325,7 +337,7 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = useCallback(() => {
|
||||||
// Reset all states
|
// Reset all states
|
||||||
setStatusOptions({
|
setStatusOptions({
|
||||||
returned: false,
|
returned: false,
|
||||||
@@ -345,7 +357,7 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
|||||||
setReplacementCost("");
|
setReplacementCost("");
|
||||||
setProofOfOwnership([]);
|
setProofOfOwnership([]);
|
||||||
onHide();
|
onHide();
|
||||||
};
|
}, [onHide]);
|
||||||
|
|
||||||
const handleBackdropClick = useCallback(
|
const handleBackdropClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -630,7 +642,7 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
|
|||||||
<div key={index} className="col-md-3 mb-2">
|
<div key={index} className="col-md-3 mb-2">
|
||||||
<div className="position-relative">
|
<div className="position-relative">
|
||||||
<img
|
<img
|
||||||
src={URL.createObjectURL(photo)}
|
src={photoBlobUrls[index]}
|
||||||
alt={`Photo ${index + 1}`}
|
alt={`Photo ${index + 1}`}
|
||||||
className="img-fluid rounded"
|
className="img-fluid rounded"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ const MyListings: React.FC = () => {
|
|||||||
checkType: string;
|
checkType: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
|
const [availableChecks, setAvailableChecks] = useState<any[]>([]);
|
||||||
const [conditionChecks, setConditionChecks] = useState<any[]>([]);
|
|
||||||
const [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
|
const [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
|
||||||
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
||||||
|
|
||||||
@@ -69,12 +68,6 @@ const MyListings: React.FC = () => {
|
|||||||
fetchAvailableChecks();
|
fetchAvailableChecks();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ownerRentals.length > 0) {
|
|
||||||
fetchConditionChecks();
|
|
||||||
}
|
|
||||||
}, [ownerRentals]);
|
|
||||||
|
|
||||||
const fetchMyListings = async () => {
|
const fetchMyListings = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
@@ -148,31 +141,6 @@ const MyListings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchConditionChecks = async () => {
|
|
||||||
try {
|
|
||||||
// Fetch condition checks for all owner rentals
|
|
||||||
const allChecks: any[] = [];
|
|
||||||
for (const rental of ownerRentals) {
|
|
||||||
try {
|
|
||||||
const response = await conditionCheckAPI.getConditionChecks(
|
|
||||||
rental.id
|
|
||||||
);
|
|
||||||
const checks = Array.isArray(response.data.conditionChecks)
|
|
||||||
? response.data.conditionChecks
|
|
||||||
: [];
|
|
||||||
allChecks.push(...checks);
|
|
||||||
} catch (err) {
|
|
||||||
// Continue even if one rental fails
|
|
||||||
console.error(`Failed to fetch checks for rental ${rental.id}:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setConditionChecks(allChecks);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("Failed to fetch condition checks:", err);
|
|
||||||
setConditionChecks([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Owner functionality handlers
|
// Owner functionality handlers
|
||||||
const handleAcceptRental = async (rentalId: string) => {
|
const handleAcceptRental = async (rentalId: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -271,7 +239,6 @@ const MyListings: React.FC = () => {
|
|||||||
|
|
||||||
const handleConditionCheckSuccess = () => {
|
const handleConditionCheckSuccess = () => {
|
||||||
fetchAvailableChecks();
|
fetchAvailableChecks();
|
||||||
fetchConditionChecks();
|
|
||||||
alert("Condition check submitted successfully!");
|
alert("Condition check submitted successfully!");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -283,20 +250,10 @@ const MyListings: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCompletedChecksForRental = (rentalId: string) => {
|
// Filter owner rentals - exclude cancelled (shown in Rental History)
|
||||||
if (!Array.isArray(conditionChecks)) return [];
|
|
||||||
return conditionChecks.filter(
|
|
||||||
(check) =>
|
|
||||||
check.rentalId === rentalId &&
|
|
||||||
(check.checkType === "pre_rental_owner" ||
|
|
||||||
check.checkType === "post_rental_owner")
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter owner rentals
|
|
||||||
const allOwnerRentals = ownerRentals
|
const allOwnerRentals = ownerRentals
|
||||||
.filter((r) =>
|
.filter((r) =>
|
||||||
["pending", "confirmed", "active", "cancelled"].includes(r.status)
|
["pending", "confirmed", "active"].includes(r.status)
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const statusOrder = { pending: 0, confirmed: 1, active: 2 };
|
const statusOrder = { pending: 0, confirmed: 1, active: 2 };
|
||||||
@@ -502,30 +459,6 @@ const MyListings: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Condition Check Status */}
|
|
||||||
{getCompletedChecksForRental(rental.id).length > 0 && (
|
|
||||||
<div className="mb-2">
|
|
||||||
{getCompletedChecksForRental(rental.id).map(
|
|
||||||
(check) => (
|
|
||||||
<div
|
|
||||||
key={`${rental.id}-${check.checkType}-status`}
|
|
||||||
className="text-success small"
|
|
||||||
>
|
|
||||||
<i className="bi bi-camera-fill me-1"></i>
|
|
||||||
{check.checkType === "pre_rental_owner"
|
|
||||||
? "Pre-Rental Check Completed"
|
|
||||||
: "Post-Rental Check Completed"}
|
|
||||||
<small className="text-muted ms-2">
|
|
||||||
{new Date(
|
|
||||||
check.createdAt
|
|
||||||
).toLocaleDateString()}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Condition Check Buttons */}
|
{/* Condition Check Buttons */}
|
||||||
{getAvailableChecksForRental(rental.id).map((check) => (
|
{getAvailableChecksForRental(rental.id).map((check) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user