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

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,
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: {
type: DataTypes.DECIMAL(10, 8)
},

View File

@@ -15,9 +15,11 @@
"dotenv": "^17.2.0",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"pg": "^8.16.3",
"sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3"
"sequelize-cli": "^6.6.3",
"uuid": "^11.1.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
@@ -134,6 +136,12 @@
"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": {
"version": "1.0.0",
"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",
"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": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -382,6 +407,21 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"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": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@@ -1209,6 +1249,15 @@
"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": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -1217,6 +1266,18 @@
"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": {
"version": "2.30.1",
"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",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -1601,6 +1723,20 @@
"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": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -1806,6 +1942,15 @@
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@@ -1951,6 +2096,23 @@
"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": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -2109,6 +2271,12 @@
"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": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
@@ -2147,12 +2315,23 @@
"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": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/validator": {

View File

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

View File

@@ -1,20 +1,20 @@
const express = require('express');
const jwt = require('jsonwebtoken');
const { User } = require('../models'); // Import from models/index.js to get models with associations
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) => {
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 }]
}
[require("sequelize").Op.or]: [{ email }, { username }],
},
});
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({
@@ -23,11 +23,11 @@ router.post('/register', async (req, res) => {
password,
firstName,
lastName,
phone
phone,
});
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
expiresIn: '7d'
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
res.status(201).json({
@@ -36,27 +36,27 @@ router.post('/register', async (req, res) => {
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
lastName: user.lastName,
},
token
token,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.post('/login', async (req, res) => {
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' });
return res.status(401).json({ error: "Invalid credentials" });
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
expiresIn: '7d'
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
res.json({
@@ -65,13 +65,13 @@ router.post('/login', async (req, res) => {
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
lastName: user.lastName,
},
token
token,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
module.exports = router;

View File

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

View File

@@ -1,6 +1,9 @@
const express = require('express');
const { User } = require('../models'); // Import from models/index.js to get models with associations
const { authenticateToken } = require('../middleware/auth');
const { uploadProfileImage } = require('../middleware/upload');
const fs = require('fs').promises;
const path = require('path');
const router = express.Router();
router.get('/profile', authenticateToken, async (req, res) => {
@@ -32,15 +35,38 @@ router.get('/:id', async (req, res) => {
router.put('/profile', authenticateToken, async (req, res) => {
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,
lastName,
phone,
address,
profileImage
});
address1,
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, {
attributes: { exclude: ['password'] }
@@ -48,8 +74,53 @@ router.put('/profile', authenticateToken, async (req, res) => {
res.json(updatedUser);
} 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;

View File

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