phone auth, image uploading, address broken up

This commit is contained in:
jackiettran
2025-07-30 19:12:56 -04:00
parent 72d79596ce
commit 7c6c120969
17 changed files with 759 additions and 182 deletions

6
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
.env
.env.*
uploads/
*.log
.DS_Store

View File

@@ -1,34 +1,33 @@
const jwt = require('jsonwebtoken'); const jwt = require("jsonwebtoken");
const { User } = require('../models'); // Import from models/index.js to get models with associations const { User } = require("../models"); // Import from models/index.js to get models with associations
const authenticateToken = async (req, res, next) => { const authenticateToken = async (req, res, next) => {
const authHeader = req.headers['authorization']; const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(' ')[1]; const token = authHeader && authHeader.split(" ")[1];
if (!token) { if (!token) {
return res.status(401).json({ error: 'Access token required' }); return res.status(401).json({ error: "Access token required" });
} }
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Handle both 'userId' and 'id' for backward compatibility const userId = decoded.id;
const userId = decoded.userId || decoded.id;
if (!userId) { if (!userId) {
return res.status(401).json({ error: 'Invalid token format' }); return res.status(401).json({ error: "Invalid token format" });
} }
const user = await User.findByPk(userId); const user = await User.findByPk(userId);
if (!user) { if (!user) {
return res.status(401).json({ error: 'User not found' }); return res.status(401).json({ error: "User not found" });
} }
req.user = user; req.user = user;
next(); next();
} catch (error) { } catch (error) {
console.error('Auth middleware error:', error); console.error("Auth middleware error:", error);
return res.status(403).json({ error: 'Invalid or expired token' }); return res.status(403).json({ error: "Invalid or expired token" });
} }
}; };

View File

@@ -0,0 +1,40 @@
const multer = require('multer');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
// Configure storage for profile images
const profileImageStorage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, '../uploads/profiles'));
},
filename: function (req, file, cb) {
// Generate unique filename: uuid + original extension
const uniqueId = uuidv4();
const ext = path.extname(file.originalname);
cb(null, `${uniqueId}${ext}`);
}
});
// File filter to accept only images
const imageFileFilter = (req, file, cb) => {
// Accept images only
const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, GIF and WebP images are allowed.'), false);
}
};
// Create multer upload middleware for profile images
const uploadProfileImage = multer({
storage: profileImageStorage,
fileFilter: imageFileFilter,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
}
}).single('profileImage');
module.exports = {
uploadProfileImage
};

View File

@@ -60,6 +60,24 @@ const Item = sequelize.define('Item', {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false
}, },
address1: {
type: DataTypes.STRING
},
address2: {
type: DataTypes.STRING
},
city: {
type: DataTypes.STRING
},
state: {
type: DataTypes.STRING
},
zipCode: {
type: DataTypes.STRING
},
country: {
type: DataTypes.STRING
},
latitude: { latitude: {
type: DataTypes.DECIMAL(10, 8) type: DataTypes.DECIMAL(10, 8)
}, },

View File

@@ -15,9 +15,11 @@
"dotenv": "^17.2.0", "dotenv": "^17.2.0",
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"pg": "^8.16.3", "pg": "^8.16.3",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3" "sequelize-cli": "^6.6.3",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
@@ -134,6 +136,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -216,6 +224,23 @@
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
}, },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -382,6 +407,21 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "dev": true
}, },
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/config-chain": { "node_modules/config-chain": {
"version": "1.1.13", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@@ -1209,6 +1249,15 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -1217,6 +1266,18 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": { "node_modules/moment": {
"version": "2.30.1", "version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@@ -1241,6 +1302,67 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"node_modules/multer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"mkdirp": "^0.5.6",
"object-assign": "^4.1.1",
"type-is": "^1.6.18",
"xtend": "^4.0.2"
},
"engines": {
"node": ">= 10.16.0"
}
},
"node_modules/multer/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -1601,6 +1723,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -1806,6 +1942,15 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/sequelize/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@@ -1951,6 +2096,23 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -2109,6 +2271,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/umzug": { "node_modules/umzug": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
@@ -2147,12 +2315,23 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
}, },
"node_modules/validator": { "node_modules/validator": {

View File

@@ -22,9 +22,11 @@
"dotenv": "^17.2.0", "dotenv": "^17.2.0",
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"pg": "^8.16.3", "pg": "^8.16.3",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3" "sequelize-cli": "^6.6.3",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.10" "nodemon": "^3.1.10"

View File

@@ -1,20 +1,20 @@
const express = require('express'); const express = require("express");
const jwt = require('jsonwebtoken'); const jwt = require("jsonwebtoken");
const { User } = require('../models'); // Import from models/index.js to get models with associations const { User } = require("../models"); // Import from models/index.js to get models with associations
const router = express.Router(); const router = express.Router();
router.post('/register', async (req, res) => { router.post("/register", async (req, res) => {
try { try {
const { username, email, password, firstName, lastName, phone } = req.body; const { username, email, password, firstName, lastName, phone } = req.body;
const existingUser = await User.findOne({ const existingUser = await User.findOne({
where: { where: {
[require('sequelize').Op.or]: [{ email }, { username }] [require("sequelize").Op.or]: [{ email }, { username }],
} },
}); });
if (existingUser) { if (existingUser) {
return res.status(400).json({ error: 'User already exists' }); return res.status(400).json({ error: "User already exists" });
} }
const user = await User.create({ const user = await User.create({
@@ -23,11 +23,11 @@ router.post('/register', async (req, res) => {
password, password,
firstName, firstName,
lastName, lastName,
phone phone,
}); });
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: '7d' expiresIn: "7d",
}); });
res.status(201).json({ res.status(201).json({
@@ -36,27 +36,27 @@ router.post('/register', async (req, res) => {
username: user.username, username: user.username,
email: user.email, email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName lastName: user.lastName,
}, },
token token,
}); });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });
router.post('/login', async (req, res) => { router.post("/login", async (req, res) => {
try { try {
const { email, password } = req.body; const { email, password } = req.body;
const user = await User.findOne({ where: { email } }); const user = await User.findOne({ where: { email } });
if (!user || !(await user.comparePassword(password))) { if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: 'Invalid credentials' }); return res.status(401).json({ error: "Invalid credentials" });
} }
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: '7d' expiresIn: "7d",
}); });
res.json({ res.json({
@@ -65,9 +65,9 @@ router.post('/login', async (req, res) => {
username: user.username, username: user.username,
email: user.email, email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName lastName: user.lastName,
}, },
token token,
}); });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });

View File

@@ -59,9 +59,7 @@ router.post("/verify-code", async (req, res) => {
const storedData = verificationCodes.get(phoneNumber); const storedData = verificationCodes.get(phoneNumber);
if (!storedData) { if (!storedData) {
return res return res.status(400).json({
.status(400)
.json({
message: "No verification code found. Please request a new one.", message: "No verification code found. Please request a new one.",
}); });
} }
@@ -69,9 +67,7 @@ router.post("/verify-code", async (req, res) => {
// Check if code expired (10 minutes) // Check if code expired (10 minutes)
if (Date.now() - storedData.createdAt > 10 * 60 * 1000) { if (Date.now() - storedData.createdAt > 10 * 60 * 1000) {
verificationCodes.delete(phoneNumber); verificationCodes.delete(phoneNumber);
return res return res.status(400).json({
.status(400)
.json({
message: "Verification code expired. Please request a new one.", message: "Verification code expired. Please request a new one.",
}); });
} }
@@ -79,9 +75,7 @@ router.post("/verify-code", async (req, res) => {
// Check attempts // Check attempts
if (storedData.attempts >= 3) { if (storedData.attempts >= 3) {
verificationCodes.delete(phoneNumber); verificationCodes.delete(phoneNumber);
return res return res.status(400).json({
.status(400)
.json({
message: "Too many failed attempts. Please request a new code.", message: "Too many failed attempts. Please request a new code.",
}); });
} }

View File

@@ -1,6 +1,9 @@
const express = require('express'); const express = require('express');
const { User } = require('../models'); // Import from models/index.js to get models with associations const { User } = require('../models'); // Import from models/index.js to get models with associations
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const { uploadProfileImage } = require('../middleware/upload');
const fs = require('fs').promises;
const path = require('path');
const router = express.Router(); const router = express.Router();
router.get('/profile', authenticateToken, async (req, res) => { router.get('/profile', authenticateToken, async (req, res) => {
@@ -32,15 +35,38 @@ router.get('/:id', async (req, res) => {
router.put('/profile', authenticateToken, async (req, res) => { router.put('/profile', authenticateToken, async (req, res) => {
try { try {
const { firstName, lastName, phone, address, profileImage } = req.body; const {
firstName,
lastName,
email,
phone,
address1,
address2,
city,
state,
zipCode,
country
} = req.body;
await req.user.update({ // Build update object, excluding empty email
const updateData = {
firstName, firstName,
lastName, lastName,
phone, phone,
address, address1,
profileImage address2,
}); city,
state,
zipCode,
country
};
// Only include email if it's not empty
if (email && email.trim() !== '') {
updateData.email = email;
}
await req.user.update(updateData);
const updatedUser = await User.findByPk(req.user.id, { const updatedUser = await User.findByPk(req.user.id, {
attributes: { exclude: ['password'] } attributes: { exclude: ['password'] }
@@ -48,8 +74,53 @@ router.put('/profile', authenticateToken, async (req, res) => {
res.json(updatedUser); res.json(updatedUser);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); console.error('Profile update error:', error);
res.status(500).json({
error: error.message,
details: error.errors ? error.errors.map(e => ({ field: e.path, message: e.message })) : undefined
});
} }
}); });
// Upload profile image endpoint
router.post('/profile/image', authenticateToken, (req, res) => {
uploadProfileImage(req, res, async (err) => {
if (err) {
console.error('Upload error:', err);
return res.status(400).json({ error: err.message });
}
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
try {
// Delete old profile image if exists
const user = await User.findByPk(req.user.id);
if (user.profileImage) {
const oldImagePath = path.join(__dirname, '../uploads/profiles', user.profileImage);
try {
await fs.unlink(oldImagePath);
} catch (unlinkErr) {
console.error('Error deleting old image:', unlinkErr);
}
}
// Update user with new image filename
await user.update({
profileImage: req.file.filename
});
res.json({
message: 'Profile image uploaded successfully',
filename: req.file.filename,
imageUrl: `/uploads/profiles/${req.file.filename}`
});
} catch (error) {
console.error('Database update error:', error);
res.status(500).json({ error: 'Failed to update profile image' });
}
});
});
module.exports = router; module.exports = router;

View File

@@ -8,6 +8,7 @@ require('dotenv').config({
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const path = require('path');
const { sequelize } = require('./models'); // Import from models/index.js to ensure associations are loaded const { sequelize } = require('./models'); // Import from models/index.js to ensure associations are loaded
const authRoutes = require('./routes/auth'); const authRoutes = require('./routes/auth');
@@ -20,8 +21,11 @@ const messageRoutes = require('./routes/messages');
const app = express(); const app = express();
app.use(cors()); app.use(cors());
app.use(bodyParser.json()); app.use(bodyParser.json({ limit: '5mb' }));
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true, limit: '5mb' }));
// Serve static files from uploads directory
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/auth/phone', phoneAuthRoutes); app.use('/api/auth/phone', phoneAuthRoutes);

View File

@@ -187,7 +187,7 @@ const AuthModal: React.FC<AuthModalProps> = ({
// Force a page reload to ensure auth state is properly initialized // Force a page reload to ensure auth state is properly initialized
// This is needed because AuthContext's useEffect only runs once on mount // This is needed because AuthContext's useEffect only runs once on mount
setTimeout(() => { setTimeout(() => {
window.location.href = '/'; window.location.href = "/";
}, 100); }, 100);
} catch (err: any) { } catch (err: any) {
console.error("Verification error:", err); console.error("Verification error:", err);

View File

@@ -1,6 +1,12 @@
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'; import React, {
import { User } from '../types'; createContext,
import { authAPI, userAPI } from '../services/api'; useState,
useContext,
useEffect,
ReactNode,
} from "react";
import { User } from "../types";
import { authAPI, userAPI } from "../services/api";
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
@@ -16,7 +22,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => { export const useAuth = () => {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (!context) { if (!context) {
throw new Error('useAuth must be used within an AuthProvider'); throw new Error("useAuth must be used within an AuthProvider");
} }
return context; return context;
}; };
@@ -30,41 +36,38 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem("token");
if (token) { if (token) {
console.log('AuthContext: Found token, fetching profile...'); userAPI
userAPI.getProfile() .getProfile()
.then(response => { .then((response) => {
console.log('AuthContext: Profile loaded', response.data);
setUser(response.data); setUser(response.data);
}) })
.catch((error) => { .catch((error) => {
console.error('AuthContext: Failed to load profile', error); localStorage.removeItem("token");
localStorage.removeItem('token');
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
} else { } else {
console.log('AuthContext: No token found');
setLoading(false); setLoading(false);
} }
}, []); }, []);
const login = async (email: string, password: string) => { const login = async (email: string, password: string) => {
const response = await authAPI.login({ email, password }); const response = await authAPI.login({ email, password });
localStorage.setItem('token', response.data.token); localStorage.setItem("token", response.data.token);
setUser(response.data.user); setUser(response.data.user);
}; };
const register = async (data: any) => { const register = async (data: any) => {
const response = await authAPI.register(data); const response = await authAPI.register(data);
localStorage.setItem('token', response.data.token); localStorage.setItem("token", response.data.token);
setUser(response.data.user); setUser(response.data.user);
}; };
const logout = () => { const logout = () => {
localStorage.removeItem('token'); localStorage.removeItem("token");
setUser(null); setUser(null);
}; };
@@ -73,7 +76,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}; };
return ( return (
<AuthContext.Provider value={{ user, loading, login, register, logout, updateUser }}> <AuthContext.Provider
value={{ user, loading, login, register, logout, updateUser }}
>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import api from "../services/api"; import api from "../services/api";
import AvailabilityCalendar from "../components/AvailabilityCalendar"; import AvailabilityCalendar from "../components/AvailabilityCalendar";
import AddressAutocomplete from "../components/AddressAutocomplete";
interface ItemFormData { interface ItemFormData {
name: string; name: string;
@@ -18,6 +17,12 @@ interface ItemFormData {
pricePerDay?: number; pricePerDay?: number;
replacementCost: number; replacementCost: number;
location: string; location: string;
address1: string;
address2: string;
city: string;
state: string;
zipCode: string;
country: string;
latitude?: number; latitude?: number;
longitude?: number; longitude?: number;
rules?: string; rules?: string;
@@ -49,6 +54,12 @@ const CreateItem: React.FC = () => {
pricePerDay: undefined, pricePerDay: undefined,
replacementCost: 0, replacementCost: 0,
location: "", location: "",
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "",
minimumRentalDays: 1, minimumRentalDays: 1,
needsTraining: false, needsTraining: false,
unavailablePeriods: [], unavailablePeriods: [],
@@ -73,8 +84,21 @@ const CreateItem: React.FC = () => {
// In production, you'd upload to a service like S3 // In production, you'd upload to a service like S3
const imageUrls = imagePreviews; const imageUrls = imagePreviews;
// Construct location from address components
const locationParts = [
formData.address1,
formData.address2,
formData.city,
formData.state,
formData.zipCode,
formData.country
].filter(part => part && part.trim());
const location = locationParts.join(', ');
const response = await api.post("/items", { const response = await api.post("/items", {
...formData, ...formData,
location,
images: imageUrls, images: imageUrls,
}); });
navigate(`/items/${response.data.id}`); navigate(`/items/${response.data.id}`);
@@ -271,23 +295,99 @@ const CreateItem: React.FC = () => {
</div> </div>
</div> </div>
<div className="mb-3"> <h6 className="mb-3">Location *</h6>
<label htmlFor="location" className="form-label">
Location * <div className="row mb-3">
<div className="col-md-6">
<label htmlFor="address1" className="form-label">
Address Line 1
</label> </label>
<AddressAutocomplete <input
id="location" type="text"
name="location" className="form-control"
value={formData.location} id="address1"
onChange={(value, lat, lon) => { name="address1"
setFormData(prev => ({ value={formData.address1}
...prev, onChange={handleChange}
location: value, placeholder="123 Main Street"
latitude: lat, required
longitude: lon />
})); </div>
}} <div className="col-md-6">
placeholder="Address" <label htmlFor="address2" className="form-label">
Address Line 2
</label>
<input
type="text"
className="form-control"
id="address2"
name="address2"
value={formData.address2}
onChange={handleChange}
placeholder="Apt, Suite, Unit, etc."
/>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="city" className="form-label">
City
</label>
<input
type="text"
className="form-control"
id="city"
name="city"
value={formData.city}
onChange={handleChange}
required
/>
</div>
<div className="col-md-3">
<label htmlFor="state" className="form-label">
State
</label>
<input
type="text"
className="form-control"
id="state"
name="state"
value={formData.state}
onChange={handleChange}
placeholder="CA"
required
/>
</div>
<div className="col-md-3">
<label htmlFor="zipCode" className="form-label">
ZIP Code
</label>
<input
type="text"
className="form-control"
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
placeholder="12345"
required
/>
</div>
</div>
<div className="mb-3">
<label htmlFor="country" className="form-label">
Country
</label>
<input
type="text"
className="form-control"
id="country"
name="country"
value={formData.country}
onChange={handleChange}
placeholder="United States"
required required
/> />
</div> </div>
@@ -305,7 +405,10 @@ const CreateItem: React.FC = () => {
/> />
<label className="form-check-label" htmlFor="pickUpAvailable"> <label className="form-check-label" htmlFor="pickUpAvailable">
Pick-Up Pick-Up
<div className="small text-muted">They pick-up the item from your location and they return the item to your location</div> <div className="small text-muted">
They pick-up the item from your location and they return the
item to your location
</div>
</label> </label>
</div> </div>
<div className="form-check"> <div className="form-check">
@@ -331,18 +434,21 @@ const CreateItem: React.FC = () => {
className="form-control form-control-sm d-inline-block mx-1" className="form-control form-control-sm d-inline-block mx-1"
id="localDeliveryRadius" id="localDeliveryRadius"
name="localDeliveryRadius" name="localDeliveryRadius"
value={formData.localDeliveryRadius || ''} value={formData.localDeliveryRadius || ""}
onChange={handleChange} onChange={handleChange}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
placeholder="25" placeholder="25"
min="1" min="1"
max="100" max="100"
style={{ width: '60px' }} style={{ width: "60px" }}
/> />
miles) miles)
</span> </span>
)} )}
<div className="small text-muted">You deliver and then pick-up the item when the rental period ends</div> <div className="small text-muted">
You deliver and then pick-up the item when the rental
period ends
</div>
</div> </div>
</label> </label>
</div> </div>
@@ -373,7 +479,9 @@ const CreateItem: React.FC = () => {
htmlFor="inPlaceUseAvailable" htmlFor="inPlaceUseAvailable"
> >
In-Place Use In-Place Use
<div className="small text-muted">They use at your location</div> <div className="small text-muted">
They use at your location
</div>
</label> </label>
</div> </div>
</div> </div>
@@ -387,7 +495,9 @@ const CreateItem: React.FC = () => {
<select <select
className="form-select" className="form-select"
value={priceType} value={priceType}
onChange={(e) => setPriceType(e.target.value as "hour" | "day")} onChange={(e) =>
setPriceType(e.target.value as "hour" | "day")
}
> >
<option value="hour">Hour</option> <option value="hour">Hour</option>
<option value="day">Day</option> <option value="day">Day</option>
@@ -400,8 +510,14 @@ const CreateItem: React.FC = () => {
type="number" type="number"
className="form-control" className="form-control"
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"} id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
name={priceType === "hour" ? "pricePerHour" : "pricePerDay"} name={
value={priceType === "hour" ? (formData.pricePerHour || "") : (formData.pricePerDay || "")} priceType === "hour" ? "pricePerHour" : "pricePerDay"
}
value={
priceType === "hour"
? formData.pricePerHour || ""
: formData.pricePerDay || ""
}
onChange={handleChange} onChange={handleChange}
step="0.01" step="0.01"
min="0" min="0"
@@ -429,11 +545,16 @@ const CreateItem: React.FC = () => {
<div className="mb-4"> <div className="mb-4">
<h5>Availability Schedule</h5> <h5>Availability Schedule</h5>
<p className="text-muted">Select dates when the item is NOT available for rent</p> <p className="text-muted">
Select dates when the item is NOT available for rent
</p>
<AvailabilityCalendar <AvailabilityCalendar
unavailablePeriods={formData.unavailablePeriods || []} unavailablePeriods={formData.unavailablePeriods || []}
onPeriodsChange={(periods) => onPeriodsChange={(periods) =>
setFormData(prev => ({ ...prev, unavailablePeriods: periods })) setFormData((prev) => ({
...prev,
unavailablePeriods: periods,
}))
} }
mode="owner" mode="owner"
/> />

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { userAPI, itemAPI, rentalAPI } from '../services/api'; import { userAPI, itemAPI, rentalAPI } from '../services/api';
import { User, Item, Rental } from '../types'; import { User, Item, Rental } from '../types';
import AddressAutocomplete from '../components/AddressAutocomplete'; import { getImageUrl } from '../utils/imageUrl';
const Profile: React.FC = () => { const Profile: React.FC = () => {
const { user, updateUser, logout } = useAuth(); const { user, updateUser, logout } = useAuth();
@@ -16,7 +16,12 @@ const Profile: React.FC = () => {
lastName: '', lastName: '',
email: '', email: '',
phone: '', phone: '',
address: '', address1: '',
address2: '',
city: '',
state: '',
zipCode: '',
country: '',
profileImage: '' profileImage: ''
}); });
const [imageFile, setImageFile] = useState<File | null>(null); const [imageFile, setImageFile] = useState<File | null>(null);
@@ -41,11 +46,16 @@ const Profile: React.FC = () => {
lastName: response.data.lastName || '', lastName: response.data.lastName || '',
email: response.data.email || '', email: response.data.email || '',
phone: response.data.phone || '', phone: response.data.phone || '',
address: response.data.address || '', address1: response.data.address1 || '',
address2: response.data.address2 || '',
city: response.data.city || '',
state: response.data.state || '',
zipCode: response.data.zipCode || '',
country: response.data.country || '',
profileImage: response.data.profileImage || '' profileImage: response.data.profileImage || ''
}); });
if (response.data.profileImage) { if (response.data.profileImage) {
setImagePreview(response.data.profileImage); setImagePreview(getImageUrl(response.data.profileImage));
} }
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.message || 'Failed to fetch profile'); setError(err.response?.data?.message || 'Failed to fetch profile');
@@ -82,15 +92,43 @@ const Profile: React.FC = () => {
setFormData(prev => ({ ...prev, [name]: value })); setFormData(prev => ({ ...prev, [name]: value }));
}; };
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
setImageFile(file); setImageFile(file);
// Show preview
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
setImagePreview(reader.result as string); setImagePreview(reader.result as string);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
// Upload image immediately
try {
const formData = new FormData();
formData.append('profileImage', file);
const response = await userAPI.uploadProfileImage(formData);
// Update the profileImage in formData with the new filename
setFormData(prev => ({
...prev,
profileImage: response.data.filename
}));
// Update preview to use the uploaded image URL
setImagePreview(getImageUrl(response.data.imageUrl));
} catch (err: any) {
console.error('Image upload error:', err);
setError(err.response?.data?.error || 'Failed to upload image');
// Reset on error
setImageFile(null);
setImagePreview(profileData?.profileImage ?
getImageUrl(profileData.profileImage) :
null
);
}
} }
}; };
@@ -100,18 +138,24 @@ const Profile: React.FC = () => {
setSuccess(null); setSuccess(null);
try { try {
const updateData = { // Don't send profileImage in the update data as it's handled separately
...formData, const { profileImage, ...updateData } = formData;
profileImage: imagePreview || formData.profileImage
};
const response = await userAPI.updateProfile(updateData); const response = await userAPI.updateProfile(updateData);
setProfileData(response.data); setProfileData(response.data);
updateUser(response.data); // Update the auth context updateUser(response.data); // Update the auth context
setSuccess('Profile updated successfully!');
setEditing(false); setEditing(false);
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.message || 'Failed to update profile'); console.error('Profile update error:', err.response?.data);
const errorMessage = err.response?.data?.error || err.response?.data?.message || 'Failed to update profile';
const errorDetails = err.response?.data?.details;
if (errorDetails && Array.isArray(errorDetails)) {
const detailMessages = errorDetails.map((d: any) => `${d.field}: ${d.message}`).join(', ');
setError(`${errorMessage} - ${detailMessages}`);
} else {
setError(errorMessage);
}
} }
}; };
@@ -126,10 +170,18 @@ const Profile: React.FC = () => {
lastName: profileData.lastName || '', lastName: profileData.lastName || '',
email: profileData.email || '', email: profileData.email || '',
phone: profileData.phone || '', phone: profileData.phone || '',
address: profileData.address || '', address1: profileData.address1 || '',
address2: profileData.address2 || '',
city: profileData.city || '',
state: profileData.state || '',
zipCode: profileData.zipCode || '',
country: profileData.country || '',
profileImage: profileData.profileImage || '' profileImage: profileData.profileImage || ''
}); });
setImagePreview(profileData.profileImage || null); setImagePreview(profileData.profileImage ?
getImageUrl(profileData.profileImage) :
null
);
} }
}; };
@@ -248,7 +300,6 @@ const Profile: React.FC = () => {
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
disabled={!editing} disabled={!editing}
required
/> />
</div> </div>
@@ -266,30 +317,88 @@ const Profile: React.FC = () => {
/> />
</div> </div>
<div className="mb-4"> <div className="row mb-3">
<label htmlFor="address" className="form-label">Address</label> <div className="col-md-6">
{editing ? ( <label htmlFor="address1" className="form-label">Address Line 1</label>
<AddressAutocomplete
id="address"
name="address"
value={formData.address}
onChange={(value) => {
setFormData(prev => ({ ...prev, address: value }));
}}
placeholder="Enter your address"
required={false}
/>
) : (
<input <input
type="text" type="text"
className="form-control" className="form-control"
id="address" id="address1"
name="address" name="address1"
value={formData.address} value={formData.address1}
disabled onChange={handleChange}
readOnly placeholder="123 Main Street"
disabled={!editing}
/>
</div>
<div className="col-md-6">
<label htmlFor="address2" className="form-label">Address Line 2</label>
<input
type="text"
className="form-control"
id="address2"
name="address2"
value={formData.address2}
onChange={handleChange}
placeholder="Apt, Suite, Unit, etc."
disabled={!editing}
/>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="city" className="form-label">City</label>
<input
type="text"
className="form-control"
id="city"
name="city"
value={formData.city}
onChange={handleChange}
disabled={!editing}
/>
</div>
<div className="col-md-3">
<label htmlFor="state" className="form-label">State</label>
<input
type="text"
className="form-control"
id="state"
name="state"
value={formData.state}
onChange={handleChange}
placeholder="CA"
disabled={!editing}
/>
</div>
<div className="col-md-3">
<label htmlFor="zipCode" className="form-label">ZIP Code</label>
<input
type="text"
className="form-control"
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
placeholder="12345"
disabled={!editing}
/>
</div>
</div>
<div className="mb-4">
<label htmlFor="country" className="form-label">Country</label>
<input
type="text"
className="form-control"
id="country"
name="country"
value={formData.country}
onChange={handleChange}
placeholder="United States"
disabled={!editing}
/> />
)}
</div> </div>
{editing ? ( {editing ? (

View File

@@ -1,16 +1,16 @@
import axios from 'axios'; import axios from "axios";
const API_BASE_URL = process.env.REACT_APP_API_URL; const API_BASE_URL = process.env.REACT_APP_API_URL;
const api = axios.create({ const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
}); });
api.interceptors.request.use((config) => { api.interceptors.request.use((config) => {
const token = localStorage.getItem('token'); const token = localStorage.getItem("token");
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
@@ -22,12 +22,12 @@ api.interceptors.response.use(
(error) => { (error) => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
// Only redirect to login if we have a token (user was logged in) // Only redirect to login if we have a token (user was logged in)
const token = localStorage.getItem('token'); const token = localStorage.getItem("token");
if (token) { if (token) {
// User was logged in but token expired/invalid // User was logged in but token expired/invalid
localStorage.removeItem('token'); localStorage.removeItem("token");
window.location.href = '/login'; window.location.href = "/login";
} }
// For non-authenticated users, just reject the error without redirecting // For non-authenticated users, just reject the error without redirecting
// Let individual components handle 401 errors as needed // Let individual components handle 401 errors as needed
@@ -37,42 +37,47 @@ api.interceptors.response.use(
); );
export const authAPI = { export const authAPI = {
register: (data: any) => api.post('/auth/register', data), register: (data: any) => api.post("/auth/register", data),
login: (data: any) => api.post('/auth/login', data), login: (data: any) => api.post("/auth/login", data),
}; };
export const userAPI = { export const userAPI = {
getProfile: () => api.get('/users/profile'), getProfile: () => api.get("/users/profile"),
updateProfile: (data: any) => api.put('/users/profile', data), updateProfile: (data: any) => api.put("/users/profile", data),
getPublicProfile: (userId: string) => api.get(`/users/${userId}`), uploadProfileImage: (formData: FormData) =>
api.post("/users/profile/image", formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
}),
getPublicProfile: (id: string) => api.get(`/users/${id}`),
}; };
export const itemAPI = { export const itemAPI = {
getItems: (params?: any) => api.get('/items', { params }), getItems: (params?: any) => api.get("/items", { params }),
getItem: (id: string) => api.get(`/items/${id}`), getItem: (id: string) => api.get(`/items/${id}`),
createItem: (data: any) => api.post('/items', data), createItem: (data: any) => api.post("/items", data),
updateItem: (id: string, data: any) => api.put(`/items/${id}`, data), updateItem: (id: string, data: any) => api.put(`/items/${id}`, data),
deleteItem: (id: string) => api.delete(`/items/${id}`), deleteItem: (id: string) => api.delete(`/items/${id}`),
getRecommendations: () => api.get('/items/recommendations'), getRecommendations: () => api.get("/items/recommendations"),
}; };
export const rentalAPI = { export const rentalAPI = {
createRental: (data: any) => api.post('/rentals', data), createRental: (data: any) => api.post("/rentals", data),
getMyRentals: () => api.get('/rentals/my-rentals'), getMyRentals: () => api.get("/rentals/my-rentals"),
getMyListings: () => api.get('/rentals/my-listings'), getMyListings: () => api.get("/rentals/my-listings"),
updateRentalStatus: (id: string, status: string) => updateRentalStatus: (id: string, status: string) =>
api.put(`/rentals/${id}/status`, { status }), api.put(`/rentals/${id}/status`, { status }),
addReview: (id: string, data: any) => addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data),
api.post(`/rentals/${id}/review`, data),
}; };
export const messageAPI = { export const messageAPI = {
getMessages: () => api.get('/messages'), getMessages: () => api.get("/messages"),
getSentMessages: () => api.get('/messages/sent'), getSentMessages: () => api.get("/messages/sent"),
getMessage: (id: string) => api.get(`/messages/${id}`), getMessage: (id: string) => api.get(`/messages/${id}`),
sendMessage: (data: any) => api.post('/messages', data), sendMessage: (data: any) => api.post("/messages", data),
markAsRead: (id: string) => api.put(`/messages/${id}/read`), markAsRead: (id: string) => api.put(`/messages/${id}/read`),
getUnreadCount: () => api.get('/messages/unread/count'), getUnreadCount: () => api.get("/messages/unread/count"),
}; };
export default api; export default api;

View File

@@ -5,7 +5,12 @@ export interface User {
firstName: string; firstName: string;
lastName: string; lastName: string;
phone?: string; phone?: string;
address?: string; address1?: string;
address2?: string;
city?: string;
state?: string;
zipCode?: string;
country?: string;
profileImage?: string; profileImage?: string;
isVerified: boolean; isVerified: boolean;
} }
@@ -42,10 +47,16 @@ export interface Item {
pricePerMonth?: number; pricePerMonth?: number;
replacementCost: number; replacementCost: number;
location: string; location: string;
address1?: string;
address2?: string;
city?: string;
state?: string;
zipCode?: string;
country?: string;
latitude?: number; latitude?: number;
longitude?: number; longitude?: number;
images: string[]; images: string[];
condition: 'excellent' | 'good' | 'fair' | 'poor'; condition: "excellent" | "good" | "fair" | "poor";
availability: boolean; availability: boolean;
specifications: Record<string, any>; specifications: Record<string, any>;
rules?: string; rules?: string;
@@ -73,9 +84,9 @@ export interface Rental {
startDate: string; startDate: string;
endDate: string; endDate: string;
totalAmount: number; totalAmount: number;
status: 'pending' | 'confirmed' | 'active' | 'completed' | 'cancelled'; status: "pending" | "confirmed" | "active" | "completed" | "cancelled";
paymentStatus: 'pending' | 'paid' | 'refunded'; paymentStatus: "pending" | "paid" | "refunded";
deliveryMethod: 'pickup' | 'delivery'; deliveryMethod: "pickup" | "delivery";
deliveryAddress?: string; deliveryAddress?: string;
notes?: string; notes?: string;
rating?: number; rating?: number;

View File

@@ -0,0 +1,13 @@
export const getImageUrl = (imagePath: string): string => {
// Get the base URL without /api
const apiUrl = process.env.REACT_APP_API_URL || '';
const baseUrl = apiUrl.replace('/api', '');
// If imagePath already includes the full path, use it
if (imagePath.startsWith('/uploads/')) {
return `${baseUrl}${imagePath}`;
}
// Otherwise, construct the full path
return `${baseUrl}/uploads/profiles/${imagePath}`;
};