fixed cors bug, separating rental confirmation for owner and renter, removing condition checks from my-listings

This commit is contained in:
jackiettran
2025-10-08 23:03:28 -04:00
parent 052781a0e6
commit 34c0ad2920
5 changed files with 249 additions and 94 deletions

View File

@@ -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({

View File

@@ -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,31 +449,68 @@ 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 {
const ownerResult = await this.sendRentalConfirmation(
owner.email, owner.email,
ownerNotification, ownerNotification,
rental 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 {
const renterResult = await this.sendRentalConfirmation(
renter.email, renter.email,
renterNotification, renterNotification,
rental rental
); );
if (renterResult.success) {
console.log( console.log(
`Rental confirmation email sent to renter: ${renter.email}` `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) { } catch (error) {
console.error("Error sending rental confirmation emails:", error); console.error(
`Failed to send rental confirmation email to renter (${renter.email}):`,
error.message
);
} }
} }
} catch (error) {
console.error(
"Error fetching user data for rental confirmation emails:",
error
);
}
return results;
}
} }
module.exports = new EmailService(); module.exports = new EmailService();

View File

@@ -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);
});
});
}); });

View File

@@ -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={{

View File

@@ -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