Initial commit - Rentall App
- Full-stack rental marketplace application - React frontend with TypeScript - Node.js/Express backend with JWT authentication - Features: item listings, rental requests, calendar availability, user profiles
This commit is contained in:
77
backend/routes/auth.js
Normal file
77
backend/routes/auth.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('../models'); // Import from models/index.js to get models with associations
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { username, email, password, firstName, lastName, phone } = req.body;
|
||||
|
||||
const existingUser = await User.findOne({
|
||||
where: {
|
||||
[require('sequelize').Op.or]: [{ email }, { username }]
|
||||
}
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'User already exists' });
|
||||
}
|
||||
|
||||
const user = await User.create({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
phone
|
||||
});
|
||||
|
||||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
|
||||
expiresIn: '7d'
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName
|
||||
},
|
||||
token
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await User.findOne({ where: { email } });
|
||||
|
||||
if (!user || !(await user.comparePassword(password))) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
|
||||
expiresIn: '7d'
|
||||
});
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName
|
||||
},
|
||||
token
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
164
backend/routes/items.js
Normal file
164
backend/routes/items.js
Normal file
@@ -0,0 +1,164 @@
|
||||
const express = require('express');
|
||||
const { Op } = require('sequelize');
|
||||
const { Item, User, Rental } = require('../models'); // Import from models/index.js to get models with associations
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
tags,
|
||||
isPortable,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
location,
|
||||
search,
|
||||
page = 1,
|
||||
limit = 20
|
||||
} = req.query;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (tags) {
|
||||
const tagsArray = Array.isArray(tags) ? tags : [tags];
|
||||
where.tags = { [Op.overlap]: tagsArray };
|
||||
}
|
||||
if (isPortable !== undefined) where.isPortable = isPortable === 'true';
|
||||
if (minPrice || maxPrice) {
|
||||
where.pricePerDay = {};
|
||||
if (minPrice) where.pricePerDay[Op.gte] = minPrice;
|
||||
if (maxPrice) where.pricePerDay[Op.lte] = maxPrice;
|
||||
}
|
||||
if (location) where.location = { [Op.iLike]: `%${location}%` };
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.iLike]: `%${search}%` } },
|
||||
{ description: { [Op.iLike]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await Item.findAndCountAll({
|
||||
where,
|
||||
include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }],
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
items: rows,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
currentPage: parseInt(page),
|
||||
totalItems: count
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/recommendations', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const userRentals = await Rental.findAll({
|
||||
where: { renterId: req.user.id },
|
||||
include: [{ model: Item, as: 'item' }]
|
||||
});
|
||||
|
||||
const rentedTags = userRentals.reduce((tags, rental) => {
|
||||
return [...tags, ...(rental.item.tags || [])];
|
||||
}, []);
|
||||
const uniqueTags = [...new Set(rentedTags)];
|
||||
|
||||
const recommendations = await Item.findAll({
|
||||
where: {
|
||||
tags: { [Op.overlap]: uniqueTags },
|
||||
availability: true
|
||||
},
|
||||
limit: 10,
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.json(recommendations);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const item = await Item.findByPk(req.params.id, {
|
||||
include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }]
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Item not found' });
|
||||
}
|
||||
|
||||
res.json(item);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const item = await Item.create({
|
||||
...req.body,
|
||||
ownerId: req.user.id
|
||||
});
|
||||
|
||||
const itemWithOwner = await Item.findByPk(item.id, {
|
||||
include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }]
|
||||
});
|
||||
|
||||
res.status(201).json(itemWithOwner);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const item = await Item.findByPk(req.params.id);
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Item not found' });
|
||||
}
|
||||
|
||||
if (item.ownerId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
await item.update(req.body);
|
||||
|
||||
const updatedItem = await Item.findByPk(item.id, {
|
||||
include: [{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }]
|
||||
});
|
||||
|
||||
res.json(updatedItem);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const item = await Item.findByPk(req.params.id);
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Item not found' });
|
||||
}
|
||||
|
||||
if (item.ownerId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
await item.destroy();
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
156
backend/routes/rentals.js
Normal file
156
backend/routes/rentals.js
Normal file
@@ -0,0 +1,156 @@
|
||||
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 } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/my-rentals', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const rentals = await Rental.findAll({
|
||||
where: { renterId: req.user.id },
|
||||
include: [
|
||||
{ model: Item, as: 'item' },
|
||||
{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.json(rentals);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/my-listings', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const rentals = await Rental.findAll({
|
||||
where: { ownerId: req.user.id },
|
||||
include: [
|
||||
{ model: Item, as: 'item' },
|
||||
{ model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] }
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.json(rentals);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemId, startDate, endDate, deliveryMethod, deliveryAddress, notes } = req.body;
|
||||
|
||||
const item = await Item.findByPk(itemId);
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Item not found' });
|
||||
}
|
||||
|
||||
if (!item.availability) {
|
||||
return res.status(400).json({ error: 'Item is not available' });
|
||||
}
|
||||
|
||||
const overlappingRental = await Rental.findOne({
|
||||
where: {
|
||||
itemId,
|
||||
status: { [Op.in]: ['confirmed', 'active'] },
|
||||
[Op.or]: [
|
||||
{
|
||||
startDate: { [Op.between]: [startDate, endDate] }
|
||||
},
|
||||
{
|
||||
endDate: { [Op.between]: [startDate, endDate] }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (overlappingRental) {
|
||||
return res.status(400).json({ error: 'Item is already booked for these dates' });
|
||||
}
|
||||
|
||||
const rentalDays = Math.ceil((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24));
|
||||
const totalAmount = rentalDays * (item.pricePerDay || 0);
|
||||
|
||||
const rental = await Rental.create({
|
||||
itemId,
|
||||
renterId: req.user.id,
|
||||
ownerId: item.ownerId,
|
||||
startDate,
|
||||
endDate,
|
||||
totalAmount,
|
||||
deliveryMethod,
|
||||
deliveryAddress,
|
||||
notes
|
||||
});
|
||||
|
||||
const rentalWithDetails = await Rental.findByPk(rental.id, {
|
||||
include: [
|
||||
{ model: Item, as: 'item' },
|
||||
{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] },
|
||||
{ model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] }
|
||||
]
|
||||
});
|
||||
|
||||
res.status(201).json(rentalWithDetails);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:id/status', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
const rental = await Rental.findByPk(req.params.id);
|
||||
|
||||
if (!rental) {
|
||||
return res.status(404).json({ error: 'Rental not found' });
|
||||
}
|
||||
|
||||
if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
await rental.update({ status });
|
||||
|
||||
const updatedRental = await Rental.findByPk(rental.id, {
|
||||
include: [
|
||||
{ model: Item, as: 'item' },
|
||||
{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] },
|
||||
{ model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] }
|
||||
]
|
||||
});
|
||||
|
||||
res.json(updatedRental);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/review', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { rating, review } = req.body;
|
||||
const rental = await Rental.findByPk(req.params.id);
|
||||
|
||||
if (!rental) {
|
||||
return res.status(404).json({ error: 'Rental not found' });
|
||||
}
|
||||
|
||||
if (rental.renterId !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Only renters can leave reviews' });
|
||||
}
|
||||
|
||||
if (rental.status !== 'completed') {
|
||||
return res.status(400).json({ error: 'Can only review completed rentals' });
|
||||
}
|
||||
|
||||
await rental.update({ rating, review });
|
||||
|
||||
res.json(rental);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
38
backend/routes/users.js
Normal file
38
backend/routes/users.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const express = require('express');
|
||||
const { User } = require('../models'); // Import from models/index.js to get models with associations
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/profile', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/profile', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { firstName, lastName, phone, address } = req.body;
|
||||
|
||||
await req.user.update({
|
||||
firstName,
|
||||
lastName,
|
||||
phone,
|
||||
address
|
||||
});
|
||||
|
||||
const updatedUser = await User.findByPk(req.user.id, {
|
||||
attributes: { exclude: ['password'] }
|
||||
});
|
||||
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user