moved private information, test fixes
This commit is contained in:
@@ -1,3 +1,27 @@
|
|||||||
|
// Mock logger module first to prevent winston initialization issues
|
||||||
|
const mockLoggerWarn = jest.fn();
|
||||||
|
const mockLoggerError = jest.fn();
|
||||||
|
const mockLoggerInfo = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('../../../utils/logger', () => ({
|
||||||
|
withRequestId: jest.fn(() => ({
|
||||||
|
warn: mockLoggerWarn,
|
||||||
|
error: mockLoggerError,
|
||||||
|
info: mockLoggerInfo
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock crypto module with both randomBytes and createHash
|
||||||
|
jest.mock('crypto', () => ({
|
||||||
|
randomBytes: jest.fn(() => ({
|
||||||
|
toString: jest.fn(() => 'mocked-hex-string-1234567890abcdef')
|
||||||
|
})),
|
||||||
|
createHash: jest.fn(() => ({
|
||||||
|
update: jest.fn().mockReturnThis(),
|
||||||
|
digest: jest.fn(() => 'mocked-hash')
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
enforceHTTPS,
|
enforceHTTPS,
|
||||||
securityHeaders,
|
securityHeaders,
|
||||||
@@ -6,13 +30,6 @@ const {
|
|||||||
sanitizeError
|
sanitizeError
|
||||||
} = require('../../../middleware/security');
|
} = require('../../../middleware/security');
|
||||||
|
|
||||||
// Mock crypto module
|
|
||||||
jest.mock('crypto', () => ({
|
|
||||||
randomBytes: jest.fn(() => ({
|
|
||||||
toString: jest.fn(() => 'mocked-hex-string-1234567890abcdef')
|
|
||||||
}))
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Security Middleware', () => {
|
describe('Security Middleware', () => {
|
||||||
let req, res, next, consoleSpy, consoleWarnSpy, consoleErrorSpy;
|
let req, res, next, consoleSpy, consoleWarnSpy, consoleErrorSpy;
|
||||||
|
|
||||||
@@ -144,13 +161,14 @@ describe('Security Middleware', () => {
|
|||||||
|
|
||||||
enforceHTTPS(req, res, next);
|
enforceHTTPS(req, res, next);
|
||||||
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
'[SECURITY] Host header mismatch during HTTPS redirect:',
|
'Host header mismatch during HTTPS redirect',
|
||||||
{
|
{
|
||||||
requestHost: 'malicious.com',
|
requestHost: 'malicious.com',
|
||||||
allowedHost: 'example.com',
|
allowedHost: 'example.com',
|
||||||
ip: '192.168.1.1',
|
ip: '192.168.1.1',
|
||||||
url: '/test-path'
|
url: '/test-path',
|
||||||
|
eventType: 'SECURITY_HOST_MISMATCH'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
||||||
@@ -161,7 +179,7 @@ describe('Security Middleware', () => {
|
|||||||
|
|
||||||
enforceHTTPS(req, res, next);
|
enforceHTTPS(req, res, next);
|
||||||
|
|
||||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
expect(mockLoggerWarn).not.toHaveBeenCalled();
|
||||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -315,25 +333,23 @@ describe('Security Middleware', () => {
|
|||||||
process.env.NODE_ENV = 'production';
|
process.env.NODE_ENV = 'production';
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log security event with JSON format', () => {
|
it('should log security event with structured data', () => {
|
||||||
const eventType = 'LOGIN_ATTEMPT';
|
const eventType = 'LOGIN_ATTEMPT';
|
||||||
const details = { username: 'testuser', success: false };
|
const details = { username: 'testuser', success: false };
|
||||||
|
|
||||||
logSecurityEvent(eventType, details, req);
|
logSecurityEvent(eventType, details, req);
|
||||||
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith('[SECURITY]', expect.any(String));
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
|
'Security event: LOGIN_ATTEMPT',
|
||||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
{
|
||||||
expect(loggedData).toEqual({
|
|
||||||
timestamp: expect.any(String),
|
|
||||||
eventType: 'LOGIN_ATTEMPT',
|
eventType: 'LOGIN_ATTEMPT',
|
||||||
requestId: 'test-request-id',
|
|
||||||
ip: '192.168.1.1',
|
ip: '192.168.1.1',
|
||||||
userAgent: 'Mozilla/5.0 Test Browser',
|
userAgent: 'Mozilla/5.0 Test Browser',
|
||||||
userId: 'anonymous',
|
userId: 'anonymous',
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
success: false
|
success: false
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include user ID when user is authenticated', () => {
|
it('should include user ID when user is authenticated', () => {
|
||||||
@@ -343,8 +359,13 @@ describe('Security Middleware', () => {
|
|||||||
|
|
||||||
logSecurityEvent(eventType, details, req);
|
logSecurityEvent(eventType, details, req);
|
||||||
|
|
||||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
expect(loggedData.userId).toBe(123);
|
'Security event: DATA_ACCESS',
|
||||||
|
expect.objectContaining({
|
||||||
|
userId: 123,
|
||||||
|
resource: '/api/users'
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing request ID', () => {
|
it('should handle missing request ID', () => {
|
||||||
@@ -354,8 +375,12 @@ describe('Security Middleware', () => {
|
|||||||
|
|
||||||
logSecurityEvent(eventType, details, req);
|
logSecurityEvent(eventType, details, req);
|
||||||
|
|
||||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
expect(loggedData.requestId).toBe('unknown');
|
'Security event: SUSPICIOUS_ACTIVITY',
|
||||||
|
expect.objectContaining({
|
||||||
|
reason: 'Multiple failed attempts'
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing IP address', () => {
|
it('should handle missing IP address', () => {
|
||||||
@@ -366,18 +391,28 @@ describe('Security Middleware', () => {
|
|||||||
|
|
||||||
logSecurityEvent(eventType, details, req);
|
logSecurityEvent(eventType, details, req);
|
||||||
|
|
||||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
expect(loggedData.ip).toBe('10.0.0.1');
|
'Security event: IP_CHECK',
|
||||||
|
expect.objectContaining({
|
||||||
|
ip: '10.0.0.1',
|
||||||
|
status: 'blocked'
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include ISO timestamp', () => {
|
it('should call logger with event type and details', () => {
|
||||||
const eventType = 'TEST_EVENT';
|
const eventType = 'TEST_EVENT';
|
||||||
const details = {};
|
const details = {};
|
||||||
|
|
||||||
logSecurityEvent(eventType, details, req);
|
logSecurityEvent(eventType, details, req);
|
||||||
|
|
||||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
expect(loggedData.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
'Security event: TEST_EVENT',
|
||||||
|
expect.objectContaining({
|
||||||
|
eventType: 'TEST_EVENT',
|
||||||
|
ip: '192.168.1.1'
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -386,28 +421,35 @@ describe('Security Middleware', () => {
|
|||||||
process.env.NODE_ENV = 'development';
|
process.env.NODE_ENV = 'development';
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log security event with simple format', () => {
|
it('should log security event using logger', () => {
|
||||||
const eventType = 'LOGIN_ATTEMPT';
|
const eventType = 'LOGIN_ATTEMPT';
|
||||||
const details = { username: 'testuser', success: false };
|
const details = { username: 'testuser', success: false };
|
||||||
|
|
||||||
logSecurityEvent(eventType, details, req);
|
logSecurityEvent(eventType, details, req);
|
||||||
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
'[SECURITY]',
|
'Security event: LOGIN_ATTEMPT',
|
||||||
'LOGIN_ATTEMPT',
|
expect.objectContaining({
|
||||||
{ username: 'testuser', success: false }
|
eventType: 'LOGIN_ATTEMPT',
|
||||||
|
username: 'testuser',
|
||||||
|
success: false
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not log JSON in development', () => {
|
it('should use structured logging in development', () => {
|
||||||
const eventType = 'TEST_EVENT';
|
const eventType = 'TEST_EVENT';
|
||||||
const details = { test: true };
|
const details = { test: true };
|
||||||
|
|
||||||
logSecurityEvent(eventType, details, req);
|
logSecurityEvent(eventType, details, req);
|
||||||
|
|
||||||
expect(consoleSpy).toHaveBeenCalledWith('[SECURITY]', 'TEST_EVENT', { test: true });
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
// Ensure it's not JSON.stringify format
|
'Security event: TEST_EVENT',
|
||||||
expect(consoleSpy).not.toHaveBeenCalledWith('[SECURITY]', expect.stringMatching(/^{.*}$/));
|
expect.objectContaining({
|
||||||
|
eventType: 'TEST_EVENT',
|
||||||
|
test: true
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -418,8 +460,12 @@ describe('Security Middleware', () => {
|
|||||||
|
|
||||||
logSecurityEvent('TEST', {}, req);
|
logSecurityEvent('TEST', {}, req);
|
||||||
|
|
||||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
expect(loggedData.userAgent).toBeNull();
|
'Security event: TEST',
|
||||||
|
expect.objectContaining({
|
||||||
|
userAgent: null
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty details object', () => {
|
it('should handle empty details object', () => {
|
||||||
@@ -427,9 +473,12 @@ describe('Security Middleware', () => {
|
|||||||
|
|
||||||
logSecurityEvent('EMPTY_DETAILS', {}, req);
|
logSecurityEvent('EMPTY_DETAILS', {}, req);
|
||||||
|
|
||||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||||
expect(loggedData.eventType).toBe('EMPTY_DETAILS');
|
'Security event: EMPTY_DETAILS',
|
||||||
expect(Object.keys(loggedData)).toContain('timestamp');
|
expect.objectContaining({
|
||||||
|
eventType: 'EMPTY_DETAILS'
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -440,36 +489,6 @@ describe('Security Middleware', () => {
|
|||||||
req.user = { id: 123 };
|
req.user = { id: 123 };
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Error logging', () => {
|
|
||||||
it('should log full error details internally', () => {
|
|
||||||
const error = new Error('Database connection failed');
|
|
||||||
error.stack = 'Error: Database connection failed\n at /app/db.js:10:5';
|
|
||||||
|
|
||||||
sanitizeError(error, req, res, next);
|
|
||||||
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Error:', {
|
|
||||||
requestId: 'test-request-id',
|
|
||||||
error: 'Database connection failed',
|
|
||||||
stack: 'Error: Database connection failed\n at /app/db.js:10:5',
|
|
||||||
userId: 123
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle missing user in logging', () => {
|
|
||||||
req.user = null;
|
|
||||||
const error = new Error('Test error');
|
|
||||||
|
|
||||||
sanitizeError(error, req, res, next);
|
|
||||||
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Error:', {
|
|
||||||
requestId: 'test-request-id',
|
|
||||||
error: 'Test error',
|
|
||||||
stack: error.stack,
|
|
||||||
userId: undefined
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Client error responses (4xx)', () => {
|
describe('Client error responses (4xx)', () => {
|
||||||
it('should handle 400 Bad Request errors', () => {
|
it('should handle 400 Bad Request errors', () => {
|
||||||
const error = new Error('Invalid input data');
|
const error = new Error('Invalid input data');
|
||||||
|
|||||||
@@ -21,6 +21,39 @@ jest.mock('../../../middleware/auth', () => ({
|
|||||||
req.user = { id: 1 };
|
req.user = { id: 1 };
|
||||||
next();
|
next();
|
||||||
}),
|
}),
|
||||||
|
requireVerifiedEmail: jest.fn((req, res, next) => next()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../utils/rentalDurationCalculator', () => ({
|
||||||
|
calculateRentalCost: jest.fn(() => 100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../services/emailService', () => ({
|
||||||
|
sendRentalRequestEmail: jest.fn(),
|
||||||
|
sendRentalApprovalEmail: jest.fn(),
|
||||||
|
sendRentalDeclinedEmail: jest.fn(),
|
||||||
|
sendRentalCompletedEmail: jest.fn(),
|
||||||
|
sendRentalCancelledEmail: jest.fn(),
|
||||||
|
sendDamageReportEmail: jest.fn(),
|
||||||
|
sendLateReturnNotificationEmail: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../utils/logger', () => ({
|
||||||
|
withRequestId: jest.fn(() => ({
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../services/lateReturnService', () => ({
|
||||||
|
calculateLateFee: jest.fn(),
|
||||||
|
processLateReturn: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../services/damageAssessmentService', () => ({
|
||||||
|
assessDamage: jest.fn(),
|
||||||
|
processDamageFee: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../../utils/feeCalculator', () => ({
|
jest.mock('../../../utils/feeCalculator', () => ({
|
||||||
@@ -47,6 +80,7 @@ jest.mock('../../../services/stripeService', () => ({
|
|||||||
|
|
||||||
const { Rental, Item, User } = require('../../../models');
|
const { Rental, Item, User } = require('../../../models');
|
||||||
const FeeCalculator = require('../../../utils/feeCalculator');
|
const FeeCalculator = require('../../../utils/feeCalculator');
|
||||||
|
const RentalDurationCalculator = require('../../../utils/rentalDurationCalculator');
|
||||||
const RefundService = require('../../../services/refundService');
|
const RefundService = require('../../../services/refundService');
|
||||||
const StripeService = require('../../../services/stripeService');
|
const StripeService = require('../../../services/stripeService');
|
||||||
|
|
||||||
@@ -267,6 +301,8 @@ describe('Rentals Routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new rental with hourly pricing', async () => {
|
it('should create a new rental with hourly pricing', async () => {
|
||||||
|
RentalDurationCalculator.calculateRentalCost.mockReturnValue(80); // 8 hours * 10/hour
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.post('/rentals')
|
.post('/rentals')
|
||||||
.send(rentalData);
|
.send(rentalData);
|
||||||
@@ -277,6 +313,8 @@ describe('Rentals Routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new rental with daily pricing', async () => {
|
it('should create a new rental with daily pricing', async () => {
|
||||||
|
RentalDurationCalculator.calculateRentalCost.mockReturnValue(150); // 3 days * 50/day
|
||||||
|
|
||||||
const dailyRentalData = {
|
const dailyRentalData = {
|
||||||
...rentalData,
|
...rentalData,
|
||||||
endDateTime: '2024-01-17T18:00:00.000Z', // 3 days
|
endDateTime: '2024-01-17T18:00:00.000Z', // 3 days
|
||||||
@@ -324,6 +362,8 @@ describe('Rentals Routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return 400 when payment method is missing for paid rentals', async () => {
|
it('should return 400 when payment method is missing for paid rentals', async () => {
|
||||||
|
RentalDurationCalculator.calculateRentalCost.mockReturnValue(100); // Paid rental
|
||||||
|
|
||||||
const dataWithoutPayment = { ...rentalData };
|
const dataWithoutPayment = { ...rentalData };
|
||||||
delete dataWithoutPayment.stripePaymentMethodId;
|
delete dataWithoutPayment.stripePaymentMethodId;
|
||||||
|
|
||||||
@@ -336,6 +376,8 @@ describe('Rentals Routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a free rental without payment method', async () => {
|
it('should create a free rental without payment method', async () => {
|
||||||
|
RentalDurationCalculator.calculateRentalCost.mockReturnValue(0); // Free rental
|
||||||
|
|
||||||
// Set up a free item (both prices are 0)
|
// Set up a free item (both prices are 0)
|
||||||
Item.findByPk.mockResolvedValue({
|
Item.findByPk.mockResolvedValue({
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -433,6 +475,11 @@ describe('Rentals Routes', () => {
|
|||||||
|
|
||||||
StripeService.chargePaymentMethod.mockResolvedValue({
|
StripeService.chargePaymentMethod.mockResolvedValue({
|
||||||
paymentIntentId: 'pi_test123',
|
paymentIntentId: 'pi_test123',
|
||||||
|
paymentMethod: {
|
||||||
|
brand: 'visa',
|
||||||
|
last4: '4242'
|
||||||
|
},
|
||||||
|
chargedAt: new Date('2024-01-15T10:00:00.000Z')
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedRental = {
|
const updatedRental = {
|
||||||
@@ -461,6 +508,9 @@ describe('Rentals Routes', () => {
|
|||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
paymentStatus: 'paid',
|
paymentStatus: 'paid',
|
||||||
stripePaymentIntentId: 'pi_test123',
|
stripePaymentIntentId: 'pi_test123',
|
||||||
|
paymentMethodBrand: 'visa',
|
||||||
|
paymentMethodLast4: '4242',
|
||||||
|
chargedAt: new Date('2024-01-15T10:00:00.000Z'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -618,7 +618,19 @@ describe('StripeService', () => {
|
|||||||
status: 'succeeded',
|
status: 'succeeded',
|
||||||
client_secret: 'pi_123456789_secret_test',
|
client_secret: 'pi_123456789_secret_test',
|
||||||
amount: 5000,
|
amount: 5000,
|
||||||
currency: 'usd'
|
currency: 'usd',
|
||||||
|
created: 1234567890,
|
||||||
|
charges: {
|
||||||
|
data: [{
|
||||||
|
payment_method_details: {
|
||||||
|
type: 'card',
|
||||||
|
card: {
|
||||||
|
brand: 'visa',
|
||||||
|
last4: '4242'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockStripePaymentIntentsCreate.mockResolvedValue(mockPaymentIntent);
|
mockStripePaymentIntentsCreate.mockResolvedValue(mockPaymentIntent);
|
||||||
@@ -636,14 +648,23 @@ describe('StripeService', () => {
|
|||||||
payment_method: 'pm_123456789',
|
payment_method: 'pm_123456789',
|
||||||
customer: 'cus_123456789',
|
customer: 'cus_123456789',
|
||||||
confirm: true,
|
confirm: true,
|
||||||
|
off_session: true,
|
||||||
return_url: 'http://localhost:3000/payment-complete',
|
return_url: 'http://localhost:3000/payment-complete',
|
||||||
metadata: { rentalId: '1' }
|
metadata: { rentalId: '1' },
|
||||||
|
expand: ['charges.data.payment_method_details']
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
paymentIntentId: 'pi_123456789',
|
paymentIntentId: 'pi_123456789',
|
||||||
status: 'succeeded',
|
status: 'succeeded',
|
||||||
clientSecret: 'pi_123456789_secret_test'
|
clientSecret: 'pi_123456789_secret_test',
|
||||||
|
paymentMethod: {
|
||||||
|
type: 'card',
|
||||||
|
brand: 'visa',
|
||||||
|
last4: '4242'
|
||||||
|
},
|
||||||
|
chargedAt: new Date(1234567890 * 1000),
|
||||||
|
amountCharged: 50.00
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const Profile: React.FC = () => {
|
|||||||
acceptedRentals: 0,
|
acceptedRentals: 0,
|
||||||
totalRentals: 0,
|
totalRentals: 0,
|
||||||
});
|
});
|
||||||
|
const [showPersonalInfo, setShowPersonalInfo] = useState(false);
|
||||||
const [availabilityData, setAvailabilityData] = useState({
|
const [availabilityData, setAvailabilityData] = useState({
|
||||||
generalAvailableAfter: "09:00",
|
generalAvailableAfter: "09:00",
|
||||||
generalAvailableBefore: "17:00",
|
generalAvailableBefore: "17:00",
|
||||||
@@ -196,14 +197,14 @@ const Profile: React.FC = () => {
|
|||||||
// Fetch past rentals as a renter
|
// Fetch past rentals as a renter
|
||||||
const renterResponse = await rentalAPI.getRentals();
|
const renterResponse = await rentalAPI.getRentals();
|
||||||
const pastRenterRentals = renterResponse.data.filter((r: Rental) =>
|
const pastRenterRentals = renterResponse.data.filter((r: Rental) =>
|
||||||
["completed", "cancelled"].includes(r.status)
|
["completed", "cancelled", "declined"].includes(r.status)
|
||||||
);
|
);
|
||||||
setPastRenterRentals(pastRenterRentals);
|
setPastRenterRentals(pastRenterRentals);
|
||||||
|
|
||||||
// Fetch past rentals as an owner
|
// Fetch past rentals as an owner
|
||||||
const ownerResponse = await rentalAPI.getListings();
|
const ownerResponse = await rentalAPI.getListings();
|
||||||
const pastOwnerRentals = ownerResponse.data.filter((r: Rental) =>
|
const pastOwnerRentals = ownerResponse.data.filter((r: Rental) =>
|
||||||
["completed", "cancelled"].includes(r.status)
|
["completed", "cancelled", "declined"].includes(r.status)
|
||||||
);
|
);
|
||||||
setPastOwnerRentals(pastOwnerRentals);
|
setPastOwnerRentals(pastOwnerRentals);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -655,15 +656,6 @@ const Profile: React.FC = () => {
|
|||||||
<i className="bi bi-clock-history me-2"></i>
|
<i className="bi bi-clock-history me-2"></i>
|
||||||
Rental History
|
Rental History
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className={`list-group-item list-group-item-action ${
|
|
||||||
activeSection === "personal-info" ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => setActiveSection("personal-info")}
|
|
||||||
>
|
|
||||||
<i className="bi bi-person me-2"></i>
|
|
||||||
Personal Information
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="list-group-item list-group-item-action text-danger"
|
className="list-group-item list-group-item-action text-danger"
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
@@ -794,6 +786,80 @@ const Profile: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Personal Information Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="d-flex align-items-center mb-3">
|
||||||
|
<h5 className="card-title mb-0">Personal Information</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-link text-primary p-0 ms-2"
|
||||||
|
onClick={() => setShowPersonalInfo(!showPersonalInfo)}
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
<i className={`bi ${showPersonalInfo ? 'bi-eye' : 'bi-eye-slash'} fs-5`}></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{showPersonalInfo && (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="email" className="form-label">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="phone" className="form-label">
|
||||||
|
Phone Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className="form-control"
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="(123) 456-7890"
|
||||||
|
disabled={!editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editing ? (
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setEditing(true)}
|
||||||
|
>
|
||||||
|
Edit Information
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Stats Card */}
|
{/* Stats Card */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
@@ -877,6 +943,8 @@ const Profile: React.FC = () => {
|
|||||||
className={`badge ${
|
className={`badge ${
|
||||||
rental.status === "completed"
|
rental.status === "completed"
|
||||||
? "bg-success"
|
? "bg-success"
|
||||||
|
: rental.status === "declined"
|
||||||
|
? "bg-secondary"
|
||||||
: "bg-danger"
|
: "bg-danger"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -1013,6 +1081,8 @@ const Profile: React.FC = () => {
|
|||||||
className={`badge ${
|
className={`badge ${
|
||||||
rental.status === "completed"
|
rental.status === "completed"
|
||||||
? "bg-success"
|
? "bg-success"
|
||||||
|
: rental.status === "declined"
|
||||||
|
? "bg-secondary"
|
||||||
: "bg-danger"
|
: "bg-danger"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -1095,72 +1165,6 @@ const Profile: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Personal Information Section */}
|
|
||||||
{activeSection === "personal-info" && (
|
|
||||||
<div>
|
|
||||||
<h4 className="mb-4">Personal Information</h4>
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-body">
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="email" className="form-label">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="form-control"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!editing}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="phone" className="form-label">
|
|
||||||
Phone Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
className="form-control"
|
|
||||||
id="phone"
|
|
||||||
name="phone"
|
|
||||||
value={formData.phone}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="(123) 456-7890"
|
|
||||||
disabled={!editing}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{editing ? (
|
|
||||||
<div className="d-flex gap-2">
|
|
||||||
<button type="submit" className="btn btn-primary">
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => setEditing(true)}
|
|
||||||
>
|
|
||||||
Edit Information
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Owner Settings Section */}
|
{/* Owner Settings Section */}
|
||||||
{activeSection === "owner-settings" && (
|
{activeSection === "owner-settings" && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -379,8 +379,13 @@ const RentItem: React.FC = () => {
|
|||||||
<div className="d-flex justify-content-between">
|
<div className="d-flex justify-content-between">
|
||||||
<strong>Total:</strong>
|
<strong>Total:</strong>
|
||||||
{costLoading ? (
|
{costLoading ? (
|
||||||
<div className="spinner-border spinner-border-sm" role="status">
|
<div
|
||||||
<span className="visually-hidden">Calculating...</span>
|
className="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span className="visually-hidden">
|
||||||
|
Calculating...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : costError ? (
|
) : costError ? (
|
||||||
<small className="text-danger">Error</small>
|
<small className="text-danger">Error</small>
|
||||||
|
|||||||
@@ -170,9 +170,9 @@ const Renting: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter rentals - show active and declined rentals
|
// Filter rentals - show only active rentals (declined go to history)
|
||||||
const renterActiveRentals = rentals.filter((r) =>
|
const renterActiveRentals = rentals.filter((r) =>
|
||||||
["pending", "confirmed", "declined", "active"].includes(r.status)
|
["pending", "confirmed", "active"].includes(r.status)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -199,7 +199,7 @@ const Renting: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-4">
|
<div className="container mt-4">
|
||||||
<h1>My Rentals</h1>
|
<h1>Rentals</h1>
|
||||||
|
|
||||||
{renterActiveRentals.length === 0 ? (
|
{renterActiveRentals.length === 0 ? (
|
||||||
<div className="text-center py-5">
|
<div className="text-center py-5">
|
||||||
@@ -301,8 +301,7 @@ const Renting: React.FC = () => {
|
|||||||
|
|
||||||
{rental.status === "declined" && rental.declineReason && (
|
{rental.status === "declined" && rental.declineReason && (
|
||||||
<div className="alert alert-warning mt-2 mb-1 p-2 small">
|
<div className="alert alert-warning mt-2 mb-1 p-2 small">
|
||||||
<strong>Decline reason:</strong>{" "}
|
<strong>Decline reason:</strong> {rental.declineReason}
|
||||||
{rental.declineReason}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user