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:
21
backend/config/database.js
Normal file
21
backend/config/database.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
process.env.DB_NAME,
|
||||
process.env.DB_USER,
|
||||
process.env.DB_PASSWORD,
|
||||
{
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: 'postgres',
|
||||
logging: false,
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = sequelize;
|
||||
27
backend/middleware/auth.js
Normal file
27
backend/middleware/auth.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('../models'); // Import from models/index.js to get models with associations
|
||||
|
||||
const authenticateToken = async (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findByPk(decoded.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(403).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { authenticateToken };
|
||||
110
backend/models/Item.js
Normal file
110
backend/models/Item.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const Item = sequelize.define('Item', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
defaultValue: []
|
||||
},
|
||||
pickUpAvailable: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
localDeliveryAvailable: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
localDeliveryRadius: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 100
|
||||
}
|
||||
},
|
||||
shippingAvailable: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
inPlaceUseAvailable: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
pricePerHour: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
pricePerDay: {
|
||||
type: DataTypes.DECIMAL(10, 2)
|
||||
},
|
||||
replacementCost: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
},
|
||||
location: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
latitude: {
|
||||
type: DataTypes.DECIMAL(10, 8)
|
||||
},
|
||||
longitude: {
|
||||
type: DataTypes.DECIMAL(11, 8)
|
||||
},
|
||||
images: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
defaultValue: []
|
||||
},
|
||||
availability: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
specifications: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
},
|
||||
rules: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
minimumRentalDays: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
},
|
||||
maximumRentalDays: {
|
||||
type: DataTypes.INTEGER
|
||||
},
|
||||
needsTraining: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
unavailablePeriods: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: []
|
||||
},
|
||||
ownerId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Item;
|
||||
76
backend/models/Rental.js
Normal file
76
backend/models/Rental.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const Rental = sequelize.define('Rental', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
itemId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Items',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
renterId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
ownerId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
startDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
endDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
totalAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'confirmed', 'active', 'completed', 'cancelled'),
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
paymentStatus: {
|
||||
type: DataTypes.ENUM('pending', 'paid', 'refunded'),
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
deliveryMethod: {
|
||||
type: DataTypes.ENUM('pickup', 'delivery'),
|
||||
defaultValue: 'pickup'
|
||||
},
|
||||
deliveryAddress: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
rating: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
},
|
||||
review: {
|
||||
type: DataTypes.TEXT
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Rental;
|
||||
66
backend/models/User.js
Normal file
66
backend/models/User.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const User = sequelize.define('User', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
firstName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
lastName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
profileImage: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
isVerified: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
}
|
||||
}, {
|
||||
hooks: {
|
||||
beforeCreate: async (user) => {
|
||||
user.password = await bcrypt.hash(user.password, 10);
|
||||
},
|
||||
beforeUpdate: async (user) => {
|
||||
if (user.changed('password')) {
|
||||
user.password = await bcrypt.hash(user.password, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
User.prototype.comparePassword = async function(password) {
|
||||
return bcrypt.compare(password, this.password);
|
||||
};
|
||||
|
||||
module.exports = User;
|
||||
22
backend/models/index.js
Normal file
22
backend/models/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const sequelize = require('../config/database');
|
||||
const User = require('./User');
|
||||
const Item = require('./Item');
|
||||
const Rental = require('./Rental');
|
||||
|
||||
User.hasMany(Item, { as: 'ownedItems', foreignKey: 'ownerId' });
|
||||
Item.belongsTo(User, { as: 'owner', foreignKey: 'ownerId' });
|
||||
|
||||
User.hasMany(Rental, { as: 'rentalsAsRenter', foreignKey: 'renterId' });
|
||||
User.hasMany(Rental, { as: 'rentalsAsOwner', foreignKey: 'ownerId' });
|
||||
|
||||
Item.hasMany(Rental, { as: 'rentals', foreignKey: 'itemId' });
|
||||
Rental.belongsTo(Item, { as: 'item', foreignKey: 'itemId' });
|
||||
Rental.belongsTo(User, { as: 'renter', foreignKey: 'renterId' });
|
||||
Rental.belongsTo(User, { as: 'owner', foreignKey: 'ownerId' });
|
||||
|
||||
module.exports = {
|
||||
sequelize,
|
||||
User,
|
||||
Item,
|
||||
Rental
|
||||
};
|
||||
2364
backend/package-lock.json
generated
Normal file
2364
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
backend/package.json
Normal file
28
backend/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.16.3",
|
||||
"sequelize": "^6.37.7",
|
||||
"sequelize-cli": "^6.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
||||
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;
|
||||
36
backend/server.js
Normal file
36
backend/server.js
Normal file
@@ -0,0 +1,36 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
const { sequelize } = require('./models'); // Import from models/index.js to ensure associations are loaded
|
||||
|
||||
const authRoutes = require('./routes/auth');
|
||||
const userRoutes = require('./routes/users');
|
||||
const itemRoutes = require('./routes/items');
|
||||
const rentalRoutes = require('./routes/rentals');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/items', itemRoutes);
|
||||
app.use('/api/rentals', rentalRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: 'Rentall API is running!' });
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
sequelize.sync({ alter: true }).then(() => {
|
||||
console.log('Database synced');
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Unable to sync database:', err);
|
||||
});
|
||||
Reference in New Issue
Block a user