Compare commits

...

3 Commits

Author SHA1 Message Date
jackiettran
f2d3aac029 sanitized errors 2025-11-26 15:49:42 -05:00
jackiettran
fab79e64ee removed console logs from frontend and a logs from locationService 2025-11-26 15:01:00 -05:00
jackiettran
8b10103ae4 csrf token handling, two jwt tokens 2025-11-26 14:25:49 -05:00
20 changed files with 571 additions and 467 deletions

View File

@@ -14,7 +14,7 @@ const authenticateToken = async (req, res, next) => {
} }
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const userId = decoded.id; const userId = decoded.id;
if (!userId) { if (!userId) {
@@ -78,7 +78,7 @@ const optionalAuth = async (req, res, next) => {
} }
try { try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const userId = decoded.id; const userId = decoded.id;
if (!userId) { if (!userId) {

View File

@@ -72,7 +72,8 @@ const getCSRFToken = (req, res) => {
maxAge: 60 * 60 * 1000, maxAge: 60 * 60 * 1000,
}); });
res.json({ csrfToken: token }); res.set("X-CSRF-Token", token);
res.status(204).send();
}; };
module.exports = { module.exports = {

View File

@@ -4067,22 +4067,43 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"debug": "^4.4.0", "debug": "^4.4.3",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.7.0",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.0", "qs": "^6.14.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.1",
"type-is": "^2.0.0" "type-is": "^2.0.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/bowser": { "node_modules/bowser": {
@@ -4181,6 +4202,7 @@
"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",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -4745,9 +4767,10 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@@ -5359,27 +5382,18 @@
} }
}, },
"node_modules/express-validator": { "node_modules/express-validator": {
"version": "7.2.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
"integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lodash": "^4.17.21", "lodash": "^4.17.21",
"validator": "~13.12.0" "validator": "~13.15.23"
}, },
"engines": { "engines": {
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/express-validator/node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/extend": { "node_modules/extend": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -5789,9 +5803,10 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "10.4.5", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
"jackspeak": "^3.1.2", "jackspeak": "^3.1.2",
@@ -6006,26 +6021,23 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"depd": "2.0.0", "depd": "~2.0.0",
"inherits": "2.0.4", "inherits": "~2.0.4",
"setprototypeof": "1.2.0", "setprototypeof": "~1.2.0",
"statuses": "2.0.1", "statuses": "~2.0.2",
"toidentifier": "1.0.1" "toidentifier": "~1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} },
}, "funding": {
"node_modules/http-errors/node_modules/statuses": { "type": "opencollective",
"version": "2.0.1", "url": "https://opencollective.com/express"
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
} }
}, },
"node_modules/http-proxy-agent": { "node_modules/http-proxy-agent": {
@@ -7145,9 +7157,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "3.14.1", "version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -8454,17 +8466,34 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "3.0.0", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.6.3", "iconv-lite": "~0.7.0",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
@@ -9812,6 +9841,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -9917,9 +9947,10 @@
} }
}, },
"node_modules/validator": { "node_modules/validator": {
"version": "13.15.15", "version": "13.15.23",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
} }

View File

@@ -128,13 +128,13 @@ router.post(
const token = jwt.sign( const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } // Short-lived access token { expiresIn: "15m" } // Short-lived access token
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET, process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" } { expiresIn: "7d" }
); );
@@ -223,13 +223,13 @@ router.post(
const token = jwt.sign( const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } // Short-lived access token { expiresIn: "15m" } // Short-lived access token
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET, process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" } { expiresIn: "7d" }
); );
@@ -392,13 +392,13 @@ router.post(
// Generate JWT tokens // Generate JWT tokens
const token = jwt.sign( const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } { expiresIn: "15m" }
); );
const refreshToken = jwt.sign( const refreshToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" }, { id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET, process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" } { expiresIn: "7d" }
); );
@@ -550,7 +550,7 @@ router.post(
}); });
} }
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET); const decoded = jwt.verify(accessToken, process.env.JWT_ACCESS_SECRET);
const user = await User.findByPk(decoded.id); const user = await User.findByPk(decoded.id);
if (!user) { if (!user) {
@@ -625,7 +625,7 @@ router.post("/refresh", async (req, res) => {
} }
// Verify refresh token // Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET); const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
if (!decoded.id || decoded.type !== "refresh") { if (!decoded.id || decoded.type !== "refresh") {
return res.status(401).json({ error: "Invalid refresh token" }); return res.status(401).json({ error: "Invalid refresh token" });
@@ -648,7 +648,7 @@ router.post("/refresh", async (req, res) => {
// Generate new access token // Generate new access token
const newAccessToken = jwt.sign( const newAccessToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion }, { id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET, process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" } { expiresIn: "15m" }
); );

View File

@@ -7,7 +7,7 @@ const emailServices = require('../services/email');
const router = express.Router(); const router = express.Router();
// Submit new feedback // Submit new feedback
router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, res) => { router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, res, next) => {
try { try {
const { feedbackText, url } = req.body; const { feedbackText, url } = req.body;
@@ -59,7 +59,7 @@ router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req,
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });

View File

@@ -40,7 +40,7 @@ const buildCommentTree = (comments, isAdmin = false) => {
}; };
// GET /api/forum/posts - Browse all posts // GET /api/forum/posts - Browse all posts
router.get('/posts', optionalAuth, async (req, res) => { router.get('/posts', optionalAuth, async (req, res, next) => {
try { try {
const { const {
search, search,
@@ -158,12 +158,12 @@ router.get('/posts', optionalAuth, async (req, res) => {
stack: error.stack, stack: error.stack,
query: req.query query: req.query
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// GET /api/forum/posts/:id - Get single post with all comments // GET /api/forum/posts/:id - Get single post with all comments
router.get('/posts/:id', optionalAuth, async (req, res) => { router.get('/posts/:id', optionalAuth, async (req, res, next) => {
try { try {
const post = await ForumPost.findByPk(req.params.id, { const post = await ForumPost.findByPk(req.params.id, {
include: [ include: [
@@ -233,12 +233,12 @@ router.get('/posts/:id', optionalAuth, async (req, res) => {
stack: error.stack, stack: error.stack,
postId: req.params.id postId: req.params.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// POST /api/forum/posts - Create new post // POST /api/forum/posts - Create new post
router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res) => { router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res, next) => {
try { try {
let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng } = req.body; let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng } = req.body;
@@ -481,12 +481,12 @@ router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res)
authorId: req.user.id, authorId: req.user.id,
postData: logger.sanitize(req.body) postData: logger.sanitize(req.body)
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PUT /api/forum/posts/:id - Update post // PUT /api/forum/posts/:id - Update post
router.put('/posts/:id', authenticateToken, async (req, res) => { router.put('/posts/:id', authenticateToken, async (req, res, next) => {
try { try {
const post = await ForumPost.findByPk(req.params.id); const post = await ForumPost.findByPk(req.params.id);
@@ -549,12 +549,12 @@ router.put('/posts/:id', authenticateToken, async (req, res) => {
postId: req.params.id, postId: req.params.id,
authorId: req.user.id authorId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// DELETE /api/forum/posts/:id - Delete post // DELETE /api/forum/posts/:id - Delete post
router.delete('/posts/:id', authenticateToken, async (req, res) => { router.delete('/posts/:id', authenticateToken, async (req, res, next) => {
try { try {
const post = await ForumPost.findByPk(req.params.id); const post = await ForumPost.findByPk(req.params.id);
@@ -586,12 +586,12 @@ router.delete('/posts/:id', authenticateToken, async (req, res) => {
postId: req.params.id, postId: req.params.id,
authorId: req.user.id authorId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PATCH /api/forum/posts/:id/status - Update post status // PATCH /api/forum/posts/:id/status - Update post status
router.patch('/posts/:id/status', authenticateToken, async (req, res) => { router.patch('/posts/:id/status', authenticateToken, async (req, res, next) => {
try { try {
const { status } = req.body; const { status } = req.body;
const post = await ForumPost.findByPk(req.params.id); const post = await ForumPost.findByPk(req.params.id);
@@ -734,12 +734,12 @@ router.patch('/posts/:id/status', authenticateToken, async (req, res) => {
postId: req.params.id, postId: req.params.id,
authorId: req.user.id authorId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PATCH /api/forum/posts/:id/accept-answer - Mark/unmark comment as accepted answer // PATCH /api/forum/posts/:id/accept-answer - Mark/unmark comment as accepted answer
router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) => { router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res, next) => {
try { try {
const { commentId } = req.body; const { commentId } = req.body;
const post = await ForumPost.findByPk(req.params.id); const post = await ForumPost.findByPk(req.params.id);
@@ -908,12 +908,12 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
postId: req.params.id, postId: req.params.id,
authorId: req.user.id authorId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// POST /api/forum/posts/:id/comments - Add comment/reply // POST /api/forum/posts/:id/comments - Add comment/reply
router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages, async (req, res) => { router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages, async (req, res, next) => {
try { try {
const { content, parentCommentId } = req.body; const { content, parentCommentId } = req.body;
const post = await ForumPost.findByPk(req.params.id); const post = await ForumPost.findByPk(req.params.id);
@@ -1073,12 +1073,12 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
postId: req.params.id, postId: req.params.id,
authorId: req.user.id authorId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PUT /api/forum/comments/:id - Edit comment // PUT /api/forum/comments/:id - Edit comment
router.put('/comments/:id', authenticateToken, async (req, res) => { router.put('/comments/:id', authenticateToken, async (req, res, next) => {
try { try {
const { content } = req.body; const { content } = req.body;
const comment = await ForumComment.findByPk(req.params.id); const comment = await ForumComment.findByPk(req.params.id);
@@ -1122,12 +1122,12 @@ router.put('/comments/:id', authenticateToken, async (req, res) => {
commentId: req.params.id, commentId: req.params.id,
authorId: req.user.id authorId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// DELETE /api/forum/comments/:id - Soft delete comment // DELETE /api/forum/comments/:id - Soft delete comment
router.delete('/comments/:id', authenticateToken, async (req, res) => { router.delete('/comments/:id', authenticateToken, async (req, res, next) => {
try { try {
const comment = await ForumComment.findByPk(req.params.id); const comment = await ForumComment.findByPk(req.params.id);
@@ -1164,12 +1164,12 @@ router.delete('/comments/:id', authenticateToken, async (req, res) => {
commentId: req.params.id, commentId: req.params.id,
authorId: req.user.id authorId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// GET /api/forum/my-posts - Get user's posts // GET /api/forum/my-posts - Get user's posts
router.get('/my-posts', authenticateToken, async (req, res) => { router.get('/my-posts', authenticateToken, async (req, res, next) => {
try { try {
const posts = await ForumPost.findAll({ const posts = await ForumPost.findAll({
where: { authorId: req.user.id }, where: { authorId: req.user.id },
@@ -1202,12 +1202,12 @@ router.get('/my-posts', authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// GET /api/forum/tags - Get all unique tags for autocomplete // GET /api/forum/tags - Get all unique tags for autocomplete
router.get('/tags', async (req, res) => { router.get('/tags', async (req, res, next) => {
try { try {
const { search } = req.query; const { search } = req.query;
@@ -1241,14 +1241,14 @@ router.get('/tags', async (req, res) => {
stack: error.stack, stack: error.stack,
query: req.query query: req.query
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// ============ ADMIN ROUTES ============ // ============ ADMIN ROUTES ============
// DELETE /api/forum/admin/posts/:id - Admin soft-delete post // DELETE /api/forum/admin/posts/:id - Admin soft-delete post
router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, res) => { router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const { reason } = req.body; const { reason } = req.body;
@@ -1321,12 +1321,12 @@ router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, r
postId: req.params.id, postId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PATCH /api/forum/admin/posts/:id/restore - Admin restore deleted post // PATCH /api/forum/admin/posts/:id/restore - Admin restore deleted post
router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async (req, res) => { router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const post = await ForumPost.findByPk(req.params.id); const post = await ForumPost.findByPk(req.params.id);
@@ -1362,12 +1362,12 @@ router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async
postId: req.params.id, postId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// DELETE /api/forum/admin/comments/:id - Admin soft-delete comment // DELETE /api/forum/admin/comments/:id - Admin soft-delete comment
router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req, res) => { router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const { reason } = req.body; const { reason } = req.body;
@@ -1449,12 +1449,12 @@ router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req
commentId: req.params.id, commentId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PATCH /api/forum/admin/comments/:id/restore - Admin restore deleted comment // PATCH /api/forum/admin/comments/:id/restore - Admin restore deleted comment
router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, async (req, res) => { router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const comment = await ForumComment.findByPk(req.params.id); const comment = await ForumComment.findByPk(req.params.id);
@@ -1500,12 +1500,12 @@ router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, asy
commentId: req.params.id, commentId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PATCH /api/forum/admin/posts/:id/close - Admin close discussion // PATCH /api/forum/admin/posts/:id/close - Admin close discussion
router.patch('/admin/posts/:id/close', authenticateToken, requireAdmin, async (req, res) => { router.patch('/admin/posts/:id/close', authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const post = await ForumPost.findByPk(req.params.id, { const post = await ForumPost.findByPk(req.params.id, {
include: [ include: [
@@ -1615,12 +1615,12 @@ router.patch('/admin/posts/:id/close', authenticateToken, requireAdmin, async (r
postId: req.params.id, postId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// PATCH /api/forum/admin/posts/:id/reopen - Admin reopen discussion // PATCH /api/forum/admin/posts/:id/reopen - Admin reopen discussion
router.patch('/admin/posts/:id/reopen', authenticateToken, requireAdmin, async (req, res) => { router.patch('/admin/posts/:id/reopen', authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const post = await ForumPost.findByPk(req.params.id); const post = await ForumPost.findByPk(req.params.id);
@@ -1655,7 +1655,7 @@ router.patch('/admin/posts/:id/reopen', authenticateToken, requireAdmin, async (
postId: req.params.id, postId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });

View File

@@ -5,7 +5,7 @@ const { authenticateToken, requireVerifiedEmail, requireAdmin, optionalAuth } =
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const router = express.Router(); const router = express.Router();
router.get("/", async (req, res) => { router.get("/", async (req, res, next) => {
try { try {
const { const {
minPrice, minPrice,
@@ -84,11 +84,11 @@ router.get("/", async (req, res) => {
stack: error.stack, stack: error.stack,
query: req.query query: req.query
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.get("/recommendations", authenticateToken, async (req, res) => { router.get("/recommendations", authenticateToken, async (req, res, next) => {
try { try {
const userRentals = await Rental.findAll({ const userRentals = await Rental.findAll({
where: { renterId: req.user.id }, where: { renterId: req.user.id },
@@ -119,12 +119,12 @@ router.get("/recommendations", authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Public endpoint to get reviews for a specific item (must come before /:id route) // Public endpoint to get reviews for a specific item (must come before /:id route)
router.get('/:id/reviews', async (req, res) => { router.get('/:id/reviews', async (req, res, next) => {
try { try {
const { Rental, User } = require('../models'); const { Rental, User } = require('../models');
@@ -169,11 +169,11 @@ router.get('/:id/reviews', async (req, res) => {
stack: error.stack, stack: error.stack,
itemId: req.params.id itemId: req.params.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.get("/:id", optionalAuth, async (req, res) => { router.get("/:id", optionalAuth, async (req, res, next) => {
try { try {
const item = await Item.findByPk(req.params.id, { const item = await Item.findByPk(req.params.id, {
include: [ include: [
@@ -226,11 +226,11 @@ router.get("/:id", optionalAuth, async (req, res) => {
stack: error.stack, stack: error.stack,
itemId: req.params.id itemId: req.params.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => { router.post("/", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
try { try {
const item = await Item.create({ const item = await Item.create({
...req.body, ...req.body,
@@ -284,11 +284,11 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
ownerId: req.user.id, ownerId: req.user.id,
itemData: logger.sanitize(req.body) itemData: logger.sanitize(req.body)
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.put("/:id", authenticateToken, async (req, res) => { router.put("/:id", authenticateToken, async (req, res, next) => {
try { try {
const item = await Item.findByPk(req.params.id); const item = await Item.findByPk(req.params.id);
@@ -327,11 +327,11 @@ router.put("/:id", authenticateToken, async (req, res) => {
itemId: req.params.id, itemId: req.params.id,
ownerId: req.user.id ownerId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.delete("/:id", authenticateToken, async (req, res) => { router.delete("/:id", authenticateToken, async (req, res, next) => {
try { try {
const item = await Item.findByPk(req.params.id); const item = await Item.findByPk(req.params.id);
@@ -360,12 +360,12 @@ router.delete("/:id", authenticateToken, async (req, res) => {
itemId: req.params.id, itemId: req.params.id,
ownerId: req.user.id ownerId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Admin endpoints // Admin endpoints
router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res) => { router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const { reason } = req.body; const { reason } = req.body;
@@ -463,11 +463,11 @@ router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res) =>
itemId: req.params.id, itemId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.patch("/admin/:id/restore", authenticateToken, requireAdmin, async (req, res) => { router.patch("/admin/:id/restore", authenticateToken, requireAdmin, async (req, res, next) => {
try { try {
const item = await Item.findByPk(req.params.id); const item = await Item.findByPk(req.params.id);
@@ -513,7 +513,7 @@ router.patch("/admin/:id/restore", authenticateToken, requireAdmin, async (req,
itemId: req.params.id, itemId: req.params.id,
adminId: req.user.id adminId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });

View File

@@ -12,7 +12,7 @@ const path = require('path');
const router = express.Router(); const router = express.Router();
// Get all messages for the current user (inbox) // Get all messages for the current user (inbox)
router.get('/', authenticateToken, async (req, res) => { router.get('/', authenticateToken, async (req, res, next) => {
try { try {
const messages = await Message.findAll({ const messages = await Message.findAll({
where: { receiverId: req.user.id }, where: { receiverId: req.user.id },
@@ -40,12 +40,12 @@ router.get('/', authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Get conversations grouped by user pairs // Get conversations grouped by user pairs
router.get('/conversations', authenticateToken, async (req, res) => { router.get('/conversations', authenticateToken, async (req, res, next) => {
try { try {
const userId = req.user.id; const userId = req.user.id;
@@ -134,12 +134,12 @@ router.get('/conversations', authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Get sent messages // Get sent messages
router.get('/sent', authenticateToken, async (req, res) => { router.get('/sent', authenticateToken, async (req, res, next) => {
try { try {
const messages = await Message.findAll({ const messages = await Message.findAll({
where: { senderId: req.user.id }, where: { senderId: req.user.id },
@@ -167,12 +167,12 @@ router.get('/sent', authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Get a single message // Get a single message
router.get('/:id', authenticateToken, async (req, res) => { router.get('/:id', authenticateToken, async (req, res, next) => {
try { try {
const message = await Message.findOne({ const message = await Message.findOne({
where: { where: {
@@ -232,12 +232,12 @@ router.get('/:id', authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
messageId: req.params.id messageId: req.params.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Send a new message // Send a new message
router.post('/', authenticateToken, uploadMessageImage, async (req, res) => { router.post('/', authenticateToken, uploadMessageImage, async (req, res, next) => {
try { try {
const { receiverId, content } = req.body; const { receiverId, content } = req.body;
@@ -309,12 +309,12 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res) => {
senderId: req.user.id, senderId: req.user.id,
receiverId: req.body.receiverId receiverId: req.body.receiverId
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Mark message as read // Mark message as read
router.put('/:id/read', authenticateToken, async (req, res) => { router.put('/:id/read', authenticateToken, async (req, res, next) => {
try { try {
const message = await Message.findOne({ const message = await Message.findOne({
where: { where: {
@@ -354,12 +354,12 @@ router.put('/:id/read', authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
messageId: req.params.id messageId: req.params.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Get unread message count // Get unread message count
router.get('/unread/count', authenticateToken, async (req, res) => { router.get('/unread/count', authenticateToken, async (req, res, next) => {
try { try {
const count = await Message.count({ const count = await Message.count({
where: { where: {
@@ -381,7 +381,7 @@ router.get('/unread/count', authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
@@ -392,7 +392,8 @@ router.get('/images/:filename',
helmet.crossOriginResourcePolicy({ policy: "cross-origin" }), helmet.crossOriginResourcePolicy({ policy: "cross-origin" }),
async (req, res) => { async (req, res) => {
try { try {
const { filename } = req.params; // Sanitize filename to prevent path traversal attacks
const filename = path.basename(req.params.filename);
// Verify user is sender or receiver of a message with this image // Verify user is sender or receiver of a message with this image
const message = await Message.findOne({ const message = await Message.findOne({

View File

@@ -932,7 +932,7 @@ router.post("/cost-preview", authenticateToken, async (req, res) => {
}); });
// Get earnings status for owner's rentals // Get earnings status for owner's rentals
router.get("/earnings/status", authenticateToken, async (req, res) => { router.get("/earnings/status", authenticateToken, async (req, res, next) => {
try { try {
const ownerRentals = await Rental.findAll({ const ownerRentals = await Rental.findAll({
where: { where: {
@@ -960,12 +960,12 @@ router.get("/earnings/status", authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id, userId: req.user.id,
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Get refund preview (what would happen if cancelled now) // Get refund preview (what would happen if cancelled now)
router.get("/:id/refund-preview", authenticateToken, async (req, res) => { router.get("/:id/refund-preview", authenticateToken, async (req, res, next) => {
try { try {
const preview = await RefundService.getRefundPreview( const preview = await RefundService.getRefundPreview(
req.params.id, req.params.id,
@@ -980,12 +980,12 @@ router.get("/:id/refund-preview", authenticateToken, async (req, res) => {
rentalId: req.params.id, rentalId: req.params.id,
userId: req.user.id, userId: req.user.id,
}); });
res.status(400).json({ error: error.message }); next(error);
} }
}); });
// Get late fee preview // Get late fee preview
router.get("/:id/late-fee-preview", authenticateToken, async (req, res) => { router.get("/:id/late-fee-preview", authenticateToken, async (req, res, next) => {
try { try {
const { actualReturnDateTime } = req.query; const { actualReturnDateTime } = req.query;
@@ -1020,12 +1020,12 @@ router.get("/:id/late-fee-preview", authenticateToken, async (req, res) => {
rentalId: req.params.id, rentalId: req.params.id,
userId: req.user.id, userId: req.user.id,
}); });
res.status(400).json({ error: error.message }); next(error);
} }
}); });
// Cancel rental with refund processing // Cancel rental with refund processing
router.post("/:id/cancel", authenticateToken, async (req, res) => { router.post("/:id/cancel", authenticateToken, async (req, res, next) => {
try { try {
const { reason } = req.body; const { reason } = req.body;
@@ -1092,12 +1092,12 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
rentalId: req.params.id, rentalId: req.params.id,
userId: req.user.id, userId: req.user.id,
}); });
res.status(400).json({ error: error.message }); next(error);
} }
}); });
// Mark item return status (owner only) // Mark item return status (owner only)
router.post("/:id/mark-return", authenticateToken, async (req, res) => { router.post("/:id/mark-return", authenticateToken, async (req, res, next) => {
try { try {
const { status, actualReturnDateTime, statusOptions } = req.body; const { status, actualReturnDateTime, statusOptions } = req.body;
const rentalId = req.params.id; const rentalId = req.params.id;
@@ -1253,12 +1253,12 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
}); });
res.status(400).json({ error: error.message }); next(error);
} }
}); });
// Report item as damaged (owner only) // Report item as damaged (owner only)
router.post("/:id/report-damage", authenticateToken, async (req, res) => { router.post("/:id/report-damage", authenticateToken, async (req, res, next) => {
try { try {
const rentalId = req.params.id; const rentalId = req.params.id;
const userId = req.user.id; const userId = req.user.id;
@@ -1290,7 +1290,7 @@ router.post("/:id/report-damage", authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
}); });
res.status(400).json({ error: error.message }); next(error);
} }
}); });

View File

@@ -6,7 +6,7 @@ const logger = require("../utils/logger");
const router = express.Router(); const router = express.Router();
// Get checkout session status // Get checkout session status
router.get("/checkout-session/:sessionId", async (req, res) => { router.get("/checkout-session/:sessionId", async (req, res, next) => {
try { try {
const { sessionId } = req.params; const { sessionId } = req.params;
@@ -32,14 +32,14 @@ router.get("/checkout-session/:sessionId", async (req, res) => {
reqLogger.error("Stripe checkout session retrieval failed", { reqLogger.error("Stripe checkout session retrieval failed", {
error: error.message, error: error.message,
stack: error.stack, stack: error.stack,
sessionId: sessionId, sessionId: req.params.sessionId,
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Create connected account // Create connected account
router.post("/accounts", authenticateToken, requireVerifiedEmail, async (req, res) => { router.post("/accounts", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
try { try {
const user = await User.findByPk(req.user.id); const user = await User.findByPk(req.user.id);
@@ -82,12 +82,12 @@ router.post("/accounts", authenticateToken, requireVerifiedEmail, async (req, re
stack: error.stack, stack: error.stack,
userId: req.user.id, userId: req.user.id,
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Generate onboarding link // Generate onboarding link
router.post("/account-links", authenticateToken, requireVerifiedEmail, async (req, res) => { router.post("/account-links", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
try { try {
const user = await User.findByPk(req.user.id); const user = await User.findByPk(req.user.id);
@@ -128,12 +128,12 @@ router.post("/account-links", authenticateToken, requireVerifiedEmail, async (re
userId: req.user.id, userId: req.user.id,
stripeConnectedAccountId: user?.stripeConnectedAccountId, stripeConnectedAccountId: user?.stripeConnectedAccountId,
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Get account status // Get account status
router.get("/account-status", authenticateToken, async (req, res) => { router.get("/account-status", authenticateToken, async (req, res, next) => {
try { try {
const user = await User.findByPk(req.user.id); const user = await User.findByPk(req.user.id);
@@ -168,7 +168,7 @@ router.get("/account-status", authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
stripeConnectedAccountId: user?.stripeConnectedAccountId, stripeConnectedAccountId: user?.stripeConnectedAccountId,
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
@@ -177,7 +177,7 @@ router.post(
"/create-setup-checkout-session", "/create-setup-checkout-session",
authenticateToken, authenticateToken,
requireVerifiedEmail, requireVerifiedEmail,
async (req, res) => { async (req, res, next) => {
try { try {
const { rentalData } = req.body; const { rentalData } = req.body;
@@ -238,7 +238,7 @@ router.post(
userId: req.user.id, userId: req.user.id,
stripeCustomerId: user?.stripeCustomerId, stripeCustomerId: user?.stripeCustomerId,
}); });
res.status(500).json({ error: error.message }); next(error);
} }
} }
); );

View File

@@ -8,7 +8,7 @@ const fs = require('fs').promises;
const path = require('path'); 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, next) => {
try { try {
const user = await User.findByPk(req.user.id, { const user = await User.findByPk(req.user.id, {
attributes: { exclude: ['password'] } attributes: { exclude: ['password'] }
@@ -27,12 +27,12 @@ router.get('/profile', authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// Address routes (must come before /:id route) // Address routes (must come before /:id route)
router.get('/addresses', authenticateToken, async (req, res) => { router.get('/addresses', authenticateToken, async (req, res, next) => {
try { try {
const addresses = await UserAddress.findAll({ const addresses = await UserAddress.findAll({
where: { userId: req.user.id }, where: { userId: req.user.id },
@@ -52,11 +52,11 @@ router.get('/addresses', authenticateToken, async (req, res) => {
stack: error.stack, stack: error.stack,
userId: req.user.id userId: req.user.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.post('/addresses', authenticateToken, async (req, res) => { router.post('/addresses', authenticateToken, async (req, res, next) => {
try { try {
const address = await userService.createUserAddress(req.user.id, req.body); const address = await userService.createUserAddress(req.user.id, req.body);
@@ -69,11 +69,11 @@ router.post('/addresses', authenticateToken, async (req, res) => {
userId: req.user.id, userId: req.user.id,
addressData: logger.sanitize(req.body) addressData: logger.sanitize(req.body)
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.put('/addresses/:id', authenticateToken, async (req, res) => { router.put('/addresses/:id', authenticateToken, async (req, res, next) => {
try { try {
const address = await userService.updateUserAddress(req.user.id, req.params.id, req.body); const address = await userService.updateUserAddress(req.user.id, req.params.id, req.body);
@@ -88,14 +88,14 @@ router.put('/addresses/:id', authenticateToken, async (req, res) => {
}); });
if (error.message === 'Address not found') { if (error.message === 'Address not found') {
return res.status(404).json({ error: error.message }); return res.status(404).json({ error: 'Address not found' });
} }
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.delete('/addresses/:id', authenticateToken, async (req, res) => { router.delete('/addresses/:id', authenticateToken, async (req, res, next) => {
try { try {
await userService.deleteUserAddress(req.user.id, req.params.id); await userService.deleteUserAddress(req.user.id, req.params.id);
@@ -110,15 +110,15 @@ router.delete('/addresses/:id', authenticateToken, async (req, res) => {
}); });
if (error.message === 'Address not found') { if (error.message === 'Address not found') {
return res.status(404).json({ error: error.message }); return res.status(404).json({ error: 'Address not found' });
} }
res.status(500).json({ error: error.message }); next(error);
} }
}); });
// User availability routes (must come before /:id route) // User availability routes (must come before /:id route)
router.get('/availability', authenticateToken, async (req, res) => { router.get('/availability', authenticateToken, async (req, res, next) => {
try { try {
const user = await User.findByPk(req.user.id, { const user = await User.findByPk(req.user.id, {
attributes: ['defaultAvailableAfter', 'defaultAvailableBefore', 'defaultSpecifyTimesPerDay', 'defaultWeeklyTimes'] attributes: ['defaultAvailableAfter', 'defaultAvailableBefore', 'defaultSpecifyTimesPerDay', 'defaultWeeklyTimes']
@@ -130,11 +130,11 @@ router.get('/availability', authenticateToken, async (req, res) => {
weeklyTimes: user.defaultWeeklyTimes weeklyTimes: user.defaultWeeklyTimes
}); });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.put('/availability', authenticateToken, async (req, res) => { router.put('/availability', authenticateToken, async (req, res, next) => {
try { try {
const { generalAvailableAfter, generalAvailableBefore, specifyTimesPerDay, weeklyTimes } = req.body; const { generalAvailableAfter, generalAvailableBefore, specifyTimesPerDay, weeklyTimes } = req.body;
@@ -149,11 +149,11 @@ router.put('/availability', authenticateToken, async (req, res) => {
res.json({ message: 'Availability updated successfully' }); res.json({ message: 'Availability updated successfully' });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res, next) => {
try { try {
const user = await User.findByPk(req.params.id, { const user = await User.findByPk(req.params.id, {
attributes: { exclude: ['password', 'email', 'phone', 'address'] } attributes: { exclude: ['password', 'email', 'phone', 'address'] }
@@ -176,11 +176,11 @@ router.get('/:id', async (req, res) => {
stack: error.stack, stack: error.stack,
requestedUserId: req.params.id requestedUserId: req.params.id
}); });
res.status(500).json({ error: error.message }); next(error);
} }
}); });
router.put('/profile', authenticateToken, async (req, res) => { router.put('/profile', authenticateToken, async (req, res, next) => {
try { try {
// Use UserService to handle update and email notification // Use UserService to handle update and email notification
const updatedUser = await userService.updateProfile(req.user.id, req.body); const updatedUser = await userService.updateProfile(req.user.id, req.body);
@@ -188,10 +188,7 @@ router.put('/profile', authenticateToken, async (req, res) => {
res.json(updatedUser); res.json(updatedUser);
} catch (error) { } catch (error) {
console.error('Profile update error:', error); console.error('Profile update error:', error);
res.status(500).json({ next(error);
error: error.message,
details: error.errors ? error.errors.map(e => ({ field: e.path, message: e.message })) : undefined
});
} }
}); });

View File

@@ -108,6 +108,7 @@ app.use(
origin: process.env.FRONTEND_URL || "http://localhost:3000", origin: process.env.FRONTEND_URL || "http://localhost:3000",
credentials: true, credentials: true,
optionsSuccessStatus: 200, optionsSuccessStatus: 200,
exposedHeaders: ["X-CSRF-Token"],
}) })
); );

View File

@@ -1,5 +1,5 @@
const { sequelize } = require('../models'); const { sequelize } = require("../models");
const { QueryTypes } = require('sequelize'); const { QueryTypes } = require("sequelize");
class LocationService { class LocationService {
/** /**
@@ -13,19 +13,13 @@ class LocationService {
*/ */
async findUsersInRadius(latitude, longitude, radiusMiles = 10) { async findUsersInRadius(latitude, longitude, radiusMiles = 10) {
if (!latitude || !longitude) { if (!latitude || !longitude) {
throw new Error('Latitude and longitude are required'); throw new Error("Latitude and longitude are required");
} }
if (radiusMiles <= 0 || radiusMiles > 100) { if (radiusMiles <= 0 || radiusMiles > 100) {
throw new Error('Radius must be between 1 and 100 miles'); throw new Error("Radius must be between 1 and 100 miles");
} }
console.log('Finding users in radius:', {
centerLatitude: latitude,
centerLongitude: longitude,
radiusMiles
});
try { try {
// Haversine formula: // Haversine formula:
// distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2)) // distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2))
@@ -62,29 +56,22 @@ class LocationService {
replacements: { replacements: {
lat: parseFloat(latitude), lat: parseFloat(latitude),
lng: parseFloat(longitude), lng: parseFloat(longitude),
radiusMiles: parseFloat(radiusMiles) radiusMiles: parseFloat(radiusMiles),
}, },
type: QueryTypes.SELECT type: QueryTypes.SELECT,
}); });
console.log('Users found in radius:', users.map(u => ({ return users.map((user) => ({
id: u.id,
userLat: u.latitude,
userLng: u.longitude,
distance: parseFloat(u.distance).toFixed(2)
})));
return users.map(user => ({
id: user.id, id: user.id,
email: user.email, email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
latitude: parseFloat(user.latitude), latitude: parseFloat(user.latitude),
longitude: parseFloat(user.longitude), longitude: parseFloat(user.longitude),
distance: parseFloat(user.distance).toFixed(2) // Round to 2 decimal places distance: parseFloat(user.distance).toFixed(2), // Round to 2 decimal places
})); }));
} catch (error) { } catch (error) {
console.error('Error finding users in radius:', error); console.error("Error finding users in radius:", error);
throw new Error(`Failed to find users in radius: ${error.message}`); throw new Error(`Failed to find users in radius: ${error.message}`);
} }
} }
@@ -105,8 +92,10 @@ class LocationService {
const a = const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.cos(this.toRadians(lat1)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2); Math.cos(this.toRadians(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c; const distance = R * c;

View File

@@ -38,8 +38,8 @@ const authenticateSocket = async (socket, next) => {
return next(new Error("Authentication required")); return next(new Error("Authentication required"));
} }
// Verify JWT // Verify JWT (access tokens only)
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const userId = decoded.id; const userId = decoded.id;
if (!userId) { if (!userId) {

View File

@@ -1,9 +1,15 @@
import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react'; import React, {
import { messageAPI, getMessageImageUrl } from '../services/api'; useState,
import { User, Message } from '../types'; useEffect,
import { useAuth } from '../contexts/AuthContext'; useLayoutEffect,
import { useSocket } from '../contexts/SocketContext'; useRef,
import TypingIndicator from './TypingIndicator'; useCallback,
} from "react";
import { messageAPI, getMessageImageUrl } from "../services/api";
import { User, Message } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { useSocket } from "../contexts/SocketContext";
import TypingIndicator from "./TypingIndicator";
interface ChatWindowProps { interface ChatWindowProps {
show: boolean; show: boolean;
@@ -12,15 +18,30 @@ interface ChatWindowProps {
onMessagesRead?: (partnerId: string, count: number) => void; onMessagesRead?: (partnerId: string, count: number) => void;
} }
const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMessagesRead }) => { const ChatWindow: React.FC<ChatWindowProps> = ({
show,
onClose,
recipient,
onMessagesRead,
}) => {
const { user: currentUser } = useAuth(); const { user: currentUser } = useAuth();
const { isConnected, joinConversation, leaveConversation, onNewMessage, onUserTyping, emitTypingStart, emitTypingStop } = useSocket(); const {
isConnected,
joinConversation,
leaveConversation,
onNewMessage,
onUserTyping,
emitTypingStart,
emitTypingStop,
} = useSocket();
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState(''); const [newMessage, setNewMessage] = useState("");
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isRecipientTyping, setIsRecipientTyping] = useState(false); const [isRecipientTyping, setIsRecipientTyping] = useState(false);
const [initialUnreadMessageIds, setInitialUnreadMessageIds] = useState<Set<string>>(new Set()); const [initialUnreadMessageIds, setInitialUnreadMessageIds] = useState<
Set<string>
>(new Set());
const [isAtBottom, setIsAtBottom] = useState(true); const [isAtBottom, setIsAtBottom] = useState(true);
const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false); const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false);
const [selectedImage, setSelectedImage] = useState<File | null>(null); const [selectedImage, setSelectedImage] = useState<File | null>(null);
@@ -52,59 +73,57 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
}, [show, recipient.id, isConnected]); }, [show, recipient.id, isConnected]);
// Create a stable callback for handling new messages // Create a stable callback for handling new messages
const handleNewMessage = useCallback(async (message: Message) => { const handleNewMessage = useCallback(
console.log('[ChatWindow] Received new_message event:', message); async (message: Message) => {
// Only add messages that are part of this conversation
// Only add messages that are part of this conversation if (
if ( (message.senderId === recipient.id &&
(message.senderId === recipient.id && message.receiverId === currentUser?.id) || message.receiverId === currentUser?.id) ||
(message.senderId === currentUser?.id && message.receiverId === recipient.id) (message.senderId === currentUser?.id &&
) { message.receiverId === recipient.id)
console.log('[ChatWindow] Message is for this conversation, adding to chat'); ) {
setMessages((prevMessages) => { setMessages((prevMessages) => {
// Check if message already exists (avoid duplicates) // Check if message already exists (avoid duplicates)
if (prevMessages.some(m => m.id === message.id)) { if (prevMessages.some((m) => m.id === message.id)) {
console.log('[ChatWindow] Message already exists, skipping'); return prevMessages;
return prevMessages; }
} return [...prevMessages, message];
console.log('[ChatWindow] Adding new message to chat'); });
return [...prevMessages, message];
}); // Mark incoming messages from recipient as read
if (
// Mark incoming messages from recipient as read message.senderId === recipient.id &&
if (message.senderId === recipient.id && message.receiverId === currentUser?.id && !message.isRead) { message.receiverId === currentUser?.id &&
console.log('[ChatWindow] Marking new incoming message as read'); !message.isRead
try { ) {
await messageAPI.markAsRead(message.id); try {
await messageAPI.markAsRead(message.id);
// Notify parent component that message was marked read
if (onMessagesRead) { // Notify parent component that message was marked read
onMessagesRead(recipient.id, 1); if (onMessagesRead) {
onMessagesRead(recipient.id, 1);
}
} catch (error) {
console.error(
`Failed to mark message ${message.id} as read:`,
error
);
} }
} catch (error) {
console.error(`Failed to mark message ${message.id} as read:`, error);
} }
} }
} else { },
console.log('[ChatWindow] Message not for this conversation, ignoring'); [recipient.id, currentUser?.id, onMessagesRead]
} );
}, [recipient.id, currentUser?.id, onMessagesRead]);
// Listen for new messages in real-time // Listen for new messages in real-time
useEffect(() => { useEffect(() => {
console.log('[ChatWindow] Message listener useEffect running', { isConnected, show, recipientId: recipient.id });
if (!isConnected || !show) { if (!isConnected || !show) {
console.log('[ChatWindow] Skipping listener setup:', { isConnected, show });
return; return;
} }
console.log('[ChatWindow] Setting up message listener for recipient:', recipient.id);
const cleanup = onNewMessage(handleNewMessage); const cleanup = onNewMessage(handleNewMessage);
return () => { return () => {
console.log('[ChatWindow] Cleaning up message listener for recipient:', recipient.id);
cleanup(); cleanup();
}; };
}, [isConnected, show, onNewMessage, handleNewMessage]); }, [isConnected, show, onNewMessage, handleNewMessage]);
@@ -143,21 +162,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
if (!loading && !hasScrolledToUnread && messages.length > 0) { if (!loading && !hasScrolledToUnread && messages.length > 0) {
if (initialUnreadMessageIds.size > 0) { if (initialUnreadMessageIds.size > 0) {
// Find the oldest unread message // Find the oldest unread message
const oldestUnread = messages.find(m => initialUnreadMessageIds.has(m.id)); const oldestUnread = messages.find((m) =>
initialUnreadMessageIds.has(m.id)
);
if (oldestUnread && messageRefs.current.has(oldestUnread.id)) { if (oldestUnread && messageRefs.current.has(oldestUnread.id)) {
console.log(`[ChatWindow] Scrolling to oldest unread message: ${oldestUnread.id}`);
messageRefs.current.get(oldestUnread.id)?.scrollIntoView({ messageRefs.current.get(oldestUnread.id)?.scrollIntoView({
behavior: 'smooth', behavior: "smooth",
block: 'start' block: "start",
}); });
} else { } else {
console.log('[ChatWindow] Unread message ref not found, scrolling to bottom');
scrollToBottom(); scrollToBottom();
} }
} else { } else {
// No unread messages, scroll to bottom // No unread messages, scroll to bottom
console.log('[ChatWindow] No unread messages, scrolling to bottom');
scrollToBottom(); scrollToBottom();
} }
setHasScrolledToUnread(true); setHasScrolledToUnread(true);
@@ -176,26 +194,33 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
// Fetch all messages between current user and recipient // Fetch all messages between current user and recipient
const [sentRes, receivedRes] = await Promise.all([ const [sentRes, receivedRes] = await Promise.all([
messageAPI.getSentMessages(), messageAPI.getSentMessages(),
messageAPI.getMessages() messageAPI.getMessages(),
]); ]);
const sentToRecipient = sentRes.data.filter((msg: Message) => msg.receiverId === recipient.id); const sentToRecipient = sentRes.data.filter(
const receivedFromRecipient = receivedRes.data.filter((msg: Message) => msg.senderId === recipient.id); (msg: Message) => msg.receiverId === recipient.id
);
const receivedFromRecipient = receivedRes.data.filter(
(msg: Message) => msg.senderId === recipient.id
);
// Combine and sort by date // Combine and sort by date
const allMessages = [...sentToRecipient, ...receivedFromRecipient].sort( const allMessages = [...sentToRecipient, ...receivedFromRecipient].sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() (a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
); );
setMessages(allMessages); setMessages(allMessages);
// Mark all unread messages from recipient as read // Mark all unread messages from recipient as read
const unreadMessages = receivedFromRecipient.filter((msg: Message) => !msg.isRead); const unreadMessages = receivedFromRecipient.filter(
(msg: Message) => !msg.isRead
);
if (unreadMessages.length > 0) { if (unreadMessages.length > 0) {
console.log(`[ChatWindow] Marking ${unreadMessages.length} messages as read`);
// Save unread message IDs for scrolling purposes // Save unread message IDs for scrolling purposes
setInitialUnreadMessageIds(new Set(unreadMessages.map((m: Message) => m.id))); setInitialUnreadMessageIds(
new Set(unreadMessages.map((m: Message) => m.id))
);
// Mark each message as read // Mark each message as read
const markReadPromises = unreadMessages.map((message: Message) => const markReadPromises = unreadMessages.map((message: Message) =>
@@ -212,28 +237,32 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch messages:', error); console.error("Failed to fetch messages:", error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const scrollToBottom = () => { const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}; };
const setMessageRef = useCallback((id: string) => (el: HTMLDivElement | null) => { const setMessageRef = useCallback(
if (el) { (id: string) => (el: HTMLDivElement | null) => {
messageRefs.current.set(id, el); if (el) {
} else { messageRefs.current.set(id, el);
messageRefs.current.delete(id); } else {
} messageRefs.current.delete(id);
}, []); }
},
[]
);
const handleScroll = () => { const handleScroll = () => {
if (!messagesContainerRef.current) return; if (!messagesContainerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current; const { scrollTop, scrollHeight, clientHeight } =
messagesContainerRef.current;
const isBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold const isBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold
setIsAtBottom(isBottom); setIsAtBottom(isBottom);
}; };
@@ -265,14 +294,14 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
// Validate file type // Validate file type
if (!file.type.startsWith('image/')) { if (!file.type.startsWith("image/")) {
alert('Please select an image file'); alert("Please select an image file");
return; return;
} }
// Validate file size (5MB) // Validate file size (5MB)
if (file.size > 5 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
alert('Image size must be less than 5MB'); alert("Image size must be less than 5MB");
return; return;
} }
@@ -291,7 +320,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
setSelectedImage(null); setSelectedImage(null);
setImagePreview(null); setImagePreview(null);
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ''; fileInputRef.current.value = "";
} }
}; };
@@ -311,20 +340,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
setSending(true); setSending(true);
const messageContent = newMessage; const messageContent = newMessage;
const imageToSend = selectedImage; const imageToSend = selectedImage;
setNewMessage(''); // Clear input immediately for better UX setNewMessage(""); // Clear input immediately for better UX
setSelectedImage(null); setSelectedImage(null);
setImagePreview(null); setImagePreview(null);
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ''; fileInputRef.current.value = "";
} }
try { try {
// Build FormData for message (with or without image) // Build FormData for message (with or without image)
const formData = new FormData(); const formData = new FormData();
formData.append('receiverId', recipient.id); formData.append("receiverId", recipient.id);
formData.append('content', messageContent || ' '); // Send space if only image formData.append("content", messageContent || " "); // Send space if only image
if (imageToSend) { if (imageToSend) {
formData.append('image', imageToSend); formData.append("image", imageToSend);
} }
const response = await messageAPI.sendMessage(formData); const response = await messageAPI.sendMessage(formData);
@@ -333,13 +362,13 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
// Socket will handle updating the receiver's chat // Socket will handle updating the receiver's chat
setMessages((prevMessages) => { setMessages((prevMessages) => {
// Avoid duplicates // Avoid duplicates
if (prevMessages.some(m => m.id === response.data.id)) { if (prevMessages.some((m) => m.id === response.data.id)) {
return prevMessages; return prevMessages;
} }
return [...prevMessages, response.data]; return [...prevMessages, response.data];
}); });
} catch (error) { } catch (error) {
console.error('Failed to send message:', error); console.error("Failed to send message:", error);
setNewMessage(messageContent); // Restore message on error setNewMessage(messageContent); // Restore message on error
setSelectedImage(imageToSend); setSelectedImage(imageToSend);
if (imageToSend) { if (imageToSend) {
@@ -362,47 +391,47 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
const formatTime = (dateString: string) => { const formatTime = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleTimeString('en-US', { return date.toLocaleTimeString("en-US", {
hour: 'numeric', hour: "numeric",
minute: '2-digit', minute: "2-digit",
hour12: true hour12: true,
}); });
}; };
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
const today = new Date(); const today = new Date();
if (date.toDateString() === today.toDateString()) { if (date.toDateString() === today.toDateString()) {
return 'Today'; return "Today";
} }
const yesterday = new Date(today); const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) { if (date.toDateString() === yesterday.toDateString()) {
return 'Yesterday'; return "Yesterday";
} }
return date.toLocaleDateString('en-US', { return date.toLocaleDateString("en-US", {
month: 'short', month: "short",
day: 'numeric', day: "numeric",
year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined year: date.getFullYear() !== today.getFullYear() ? "numeric" : undefined,
}); });
}; };
if (!show) return null; if (!show) return null;
return ( return (
<div <div
className="position-fixed bottom-0 end-0 m-3 shadow-lg d-flex flex-column" className="position-fixed bottom-0 end-0 m-3 shadow-lg d-flex flex-column"
style={{ style={{
width: '350px', width: "350px",
height: '500px', height: "500px",
maxHeight: 'calc(100vh - 100px)', maxHeight: "calc(100vh - 100px)",
zIndex: 1050, zIndex: 1050,
borderRadius: '12px', borderRadius: "12px",
overflow: 'hidden', overflow: "hidden",
backgroundColor: 'white' backgroundColor: "white",
}} }}
> >
{/* Header */} {/* Header */}
@@ -413,23 +442,25 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
src={recipient.profileImage} src={recipient.profileImage}
alt={`${recipient.firstName} ${recipient.lastName}`} alt={`${recipient.firstName} ${recipient.lastName}`}
className="rounded-circle me-2" className="rounded-circle me-2"
style={{ width: '35px', height: '35px', objectFit: 'cover' }} style={{ width: "35px", height: "35px", objectFit: "cover" }}
/> />
) : ( ) : (
<div <div
className="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center me-2" className="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center me-2"
style={{ width: '35px', height: '35px' }} style={{ width: "35px", height: "35px" }}
> >
<i className="bi bi-person-fill text-white"></i> <i className="bi bi-person-fill text-white"></i>
</div> </div>
)} )}
<div> <div>
<h6 className="mb-0">{recipient.firstName} {recipient.lastName}</h6> <h6 className="mb-0">
{recipient.firstName} {recipient.lastName}
</h6>
</div> </div>
</div> </div>
<button <button
type="button" type="button"
className="btn-close btn-close-white" className="btn-close btn-close-white"
onClick={onClose} onClick={onClose}
></button> ></button>
</div> </div>
@@ -440,8 +471,8 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
onScroll={handleScroll} onScroll={handleScroll}
className="p-3 overflow-auto flex-grow-1" className="p-3 overflow-auto flex-grow-1"
style={{ style={{
backgroundColor: '#f8f9fa', backgroundColor: "#f8f9fa",
minHeight: 0 minHeight: 0,
}} }}
> >
{loading ? ( {loading ? (
@@ -452,15 +483,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
</div> </div>
) : messages.length === 0 ? ( ) : messages.length === 0 ? (
<div className="text-center py-5"> <div className="text-center py-5">
<i className="bi bi-chat-dots" style={{ fontSize: '3rem', color: '#dee2e6' }}></i> <i
<p className="text-muted mt-2">Start a conversation with {recipient.firstName}</p> className="bi bi-chat-dots"
style={{ fontSize: "3rem", color: "#dee2e6" }}
></i>
<p className="text-muted mt-2">
Start a conversation with {recipient.firstName}
</p>
</div> </div>
) : ( ) : (
<> <>
{messages.map((message, index) => { {messages.map((message, index) => {
const isCurrentUser = message.senderId === currentUser?.id; const isCurrentUser = message.senderId === currentUser?.id;
const showDate = index === 0 || const showDate =
formatDate(message.createdAt) !== formatDate(messages[index - 1].createdAt); index === 0 ||
formatDate(message.createdAt) !==
formatDate(messages[index - 1].createdAt);
return ( return (
<div key={message.id} ref={setMessageRef(message.id)}> <div key={message.id} ref={setMessageRef(message.id)}>
@@ -471,16 +509,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
</small> </small>
</div> </div>
)} )}
<div className={`d-flex mb-2 ${isCurrentUser ? 'justify-content-end' : ''}`}> <div
className={`d-flex mb-2 ${
isCurrentUser ? "justify-content-end" : ""
}`}
>
<div <div
className={`px-3 py-2 rounded-3 ${ className={`px-3 py-2 rounded-3 ${
isCurrentUser isCurrentUser
? 'bg-primary text-white' ? "bg-primary text-white"
: 'bg-white border' : "bg-white border"
}`} }`}
style={{ style={{
maxWidth: '75%', maxWidth: "75%",
wordBreak: 'break-word' wordBreak: "break-word",
}} }}
> >
{message.imagePath && ( {message.imagePath && (
@@ -489,24 +531,29 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
src={getMessageImageUrl(message.imagePath)} src={getMessageImageUrl(message.imagePath)}
alt="Shared image" alt="Shared image"
style={{ style={{
width: '100%', width: "100%",
borderRadius: '8px', borderRadius: "8px",
cursor: 'pointer', cursor: "pointer",
maxHeight: '300px', maxHeight: "300px",
objectFit: 'cover' objectFit: "cover",
}} }}
onClick={() => window.open(getMessageImageUrl(message.imagePath!), '_blank')} onClick={() =>
window.open(
getMessageImageUrl(message.imagePath!),
"_blank"
)
}
/> />
</div> </div>
)} )}
{message.content.trim() && ( {message.content.trim() && (
<p className="mb-1" style={{ fontSize: '0.95rem' }}> <p className="mb-1" style={{ fontSize: "0.95rem" }}>
{message.content} {message.content}
</p> </p>
)} )}
<small <small
className={isCurrentUser ? 'opacity-75' : 'text-muted'} className={isCurrentUser ? "opacity-75" : "text-muted"}
style={{ fontSize: '0.75rem' }} style={{ fontSize: "0.75rem" }}
> >
{formatTime(message.createdAt)} {formatTime(message.createdAt)}
</small> </small>
@@ -536,17 +583,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
src={imagePreview} src={imagePreview}
alt="Preview" alt="Preview"
style={{ style={{
maxWidth: '150px', maxWidth: "150px",
maxHeight: '150px', maxHeight: "150px",
borderRadius: '8px', borderRadius: "8px",
objectFit: 'cover' objectFit: "cover",
}} }}
/> />
<button <button
type="button" type="button"
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 rounded-circle" className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 rounded-circle"
onClick={handleRemoveImage} onClick={handleRemoveImage}
style={{ width: '24px', height: '24px', padding: '0', fontSize: '0.7rem' }} style={{
width: "24px",
height: "24px",
padding: "0",
fontSize: "0.7rem",
}}
> >
<i className="bi bi-x"></i> <i className="bi bi-x"></i>
</button> </button>
@@ -558,7 +610,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
type="file" type="file"
accept="image/*" accept="image/*"
onChange={handleImageSelect} onChange={handleImageSelect}
style={{ display: 'none' }} style={{ display: "none" }}
/> />
<button <button
type="button" type="button"
@@ -584,7 +636,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
disabled={sending || (!newMessage.trim() && !selectedImage)} disabled={sending || (!newMessage.trim() && !selectedImage)}
> >
{sending ? ( {sending ? (
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span
className="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
) : ( ) : (
<i className="bi bi-send-fill"></i> <i className="bi bi-send-fill"></i>
)} )}
@@ -595,4 +651,4 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
); );
}; };
export default ChatWindow; export default ChatWindow;

View File

@@ -1,6 +1,13 @@
import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react'; import React, {
import { Socket } from 'socket.io-client'; createContext,
import socketService from '../services/socket'; useContext,
useEffect,
useState,
useCallback,
ReactNode,
} from "react";
import { Socket } from "socket.io-client";
import socketService from "../services/socket";
/** /**
* Socket Context Type * Socket Context Type
@@ -14,8 +21,20 @@ interface SocketContextType {
emitTypingStop: (receiverId: string) => void; emitTypingStop: (receiverId: string) => void;
emitMarkMessageRead: (messageId: string, senderId: string) => void; emitMarkMessageRead: (messageId: string, senderId: string) => void;
onNewMessage: (callback: (message: any) => void) => () => void; onNewMessage: (callback: (message: any) => void) => () => void;
onMessageRead: (callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => () => void; onMessageRead: (
onUserTyping: (callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => () => void; callback: (data: {
messageId: string;
readAt: string;
readBy: string;
}) => void
) => () => void;
onUserTyping: (
callback: (data: {
userId: string;
firstName: string;
isTyping: boolean;
}) => void
) => () => void;
} }
/** /**
@@ -37,7 +56,7 @@ interface SocketProviderProps {
*/ */
export const SocketProvider: React.FC<SocketProviderProps> = ({ export const SocketProvider: React.FC<SocketProviderProps> = ({
children, children,
isAuthenticated = false isAuthenticated = false,
}) => { }) => {
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
@@ -46,27 +65,20 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
* Initialize socket connection when user is authenticated * Initialize socket connection when user is authenticated
*/ */
useEffect(() => { useEffect(() => {
console.log('[SocketProvider] useEffect running', { isAuthenticated });
if (!isAuthenticated) { if (!isAuthenticated) {
console.log('[SocketProvider] Not authenticated, skipping socket setup');
return; return;
} }
console.log('[SocketProvider] Initializing socket connection');
const newSocket = socketService.connect(); const newSocket = socketService.connect();
setSocket(newSocket); setSocket(newSocket);
// Listen for connection status changes // Listen for connection status changes
console.log('[SocketProvider] Setting up connection listener');
const removeListener = socketService.addConnectionListener((connected) => { const removeListener = socketService.addConnectionListener((connected) => {
console.log('[SocketProvider] Connection status changed:', connected);
setIsConnected(connected); setIsConnected(connected);
}); });
// Cleanup on unmount // Cleanup on unmount
return () => { return () => {
console.log('[SocketProvider] Cleaning up connection listener');
removeListener(); removeListener();
}; };
}, [isAuthenticated]); }, [isAuthenticated]);
@@ -76,7 +88,6 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
*/ */
useEffect(() => { useEffect(() => {
if (!isAuthenticated && socket) { if (!isAuthenticated && socket) {
console.log('[SocketProvider] User logged out, disconnecting socket');
socketService.disconnect(); socketService.disconnect();
setSocket(null); setSocket(null);
setIsConnected(false); setIsConnected(false);
@@ -114,9 +125,12 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
/** /**
* Emit mark message as read event * Emit mark message as read event
*/ */
const emitMarkMessageRead = useCallback((messageId: string, senderId: string) => { const emitMarkMessageRead = useCallback(
socketService.emitMarkMessageRead(messageId, senderId); (messageId: string, senderId: string) => {
}, []); socketService.emitMarkMessageRead(messageId, senderId);
},
[]
);
/** /**
* Listen for new messages * Listen for new messages
@@ -128,16 +142,34 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
/** /**
* Listen for message read events * Listen for message read events
*/ */
const onMessageRead = useCallback((callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => { const onMessageRead = useCallback(
return socketService.onMessageRead(callback); (
}, []); callback: (data: {
messageId: string;
readAt: string;
readBy: string;
}) => void
) => {
return socketService.onMessageRead(callback);
},
[]
);
/** /**
* Listen for typing indicators * Listen for typing indicators
*/ */
const onUserTyping = useCallback((callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => { const onUserTyping = useCallback(
return socketService.onUserTyping(callback); (
}, []); callback: (data: {
userId: string;
firstName: string;
isTyping: boolean;
}) => void
) => {
return socketService.onUserTyping(callback);
},
[]
);
const value: SocketContextType = { const value: SocketContextType = {
socket, socket,
@@ -153,9 +185,7 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
}; };
return ( return (
<SocketContext.Provider value={value}> <SocketContext.Provider value={value}>{children}</SocketContext.Provider>
{children}
</SocketContext.Provider>
); );
}; };
@@ -167,7 +197,7 @@ export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext); const context = useContext(SocketContext);
if (context === undefined) { if (context === undefined) {
throw new Error('useSocket must be used within a SocketProvider'); throw new Error("useSocket must be used within a SocketProvider");
} }
return context; return context;

View File

@@ -208,19 +208,11 @@ const ItemDetail: React.FC = () => {
const dayTimes = item.weeklyTimes[dayName]; const dayTimes = item.weeklyTimes[dayName];
availableAfter = dayTimes.availableAfter; availableAfter = dayTimes.availableAfter;
availableBefore = dayTimes.availableBefore; availableBefore = dayTimes.availableBefore;
console.log("Using day-specific times:", {
availableAfter,
availableBefore,
});
} }
// Otherwise use global times // Otherwise use global times
else if (item.availableAfter && item.availableBefore) { else if (item.availableAfter && item.availableBefore) {
availableAfter = item.availableAfter; availableAfter = item.availableAfter;
availableBefore = item.availableBefore; availableBefore = item.availableBefore;
} else {
console.log(
"No time constraints found, using default 24-hour availability"
);
} }
} }
@@ -249,7 +241,6 @@ const ItemDetail: React.FC = () => {
// If no options are available, return at least one option to prevent empty dropdown // If no options are available, return at least one option to prevent empty dropdown
if (options.length === 0) { if (options.length === 0) {
console.log("No valid time options found, showing Not Available");
options.push({ value: "00:00", label: "Not Available" }); options.push({ value: "00:00", label: "Not Available" });
} }

View File

@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { Conversation, Message, User } from '../types'; import { Conversation, Message, User } from "../types";
import { messageAPI } from '../services/api'; import { messageAPI } from "../services/api";
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from "../contexts/AuthContext";
import { useSocket } from '../contexts/SocketContext'; import { useSocket } from "../contexts/SocketContext";
import ChatWindow from '../components/ChatWindow'; import ChatWindow from "../components/ChatWindow";
const Messages: React.FC = () => { const Messages: React.FC = () => {
const { user } = useAuth(); const { user } = useAuth();
@@ -23,17 +23,16 @@ const Messages: React.FC = () => {
if (!isConnected) return; if (!isConnected) return;
const cleanup = onNewMessage((newMessage: Message) => { const cleanup = onNewMessage((newMessage: Message) => {
console.log('[Messages] Received new message:', newMessage);
setConversations((prevConversations) => { setConversations((prevConversations) => {
// Determine conversation partner // Determine conversation partner
const partnerId = newMessage.senderId === user?.id const partnerId =
? newMessage.receiverId newMessage.senderId === user?.id
: newMessage.senderId; ? newMessage.receiverId
: newMessage.senderId;
// Find existing conversation // Find existing conversation
const existingIndex = prevConversations.findIndex( const existingIndex = prevConversations.findIndex(
c => c.partnerId === partnerId (c) => c.partnerId === partnerId
); );
if (existingIndex !== -1) { if (existingIndex !== -1) {
@@ -46,7 +45,7 @@ const Messages: React.FC = () => {
content: newMessage.content, content: newMessage.content,
senderId: newMessage.senderId, senderId: newMessage.senderId,
createdAt: newMessage.createdAt, createdAt: newMessage.createdAt,
isRead: newMessage.isRead isRead: newMessage.isRead,
}; };
conv.lastMessageAt = newMessage.createdAt; conv.lastMessageAt = newMessage.createdAt;
@@ -58,20 +57,21 @@ const Messages: React.FC = () => {
updated[existingIndex] = conv; updated[existingIndex] = conv;
// Re-sort by most recent // Re-sort by most recent
updated.sort((a, b) => updated.sort(
new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime() (a, b) =>
new Date(b.lastMessageAt).getTime() -
new Date(a.lastMessageAt).getTime()
); );
console.log('[Messages] Updated existing conversation');
return updated; return updated;
} else { } else {
// New conversation - add to top // New conversation - add to top
const partner = newMessage.senderId === user?.id const partner =
? newMessage.receiver! newMessage.senderId === user?.id
: newMessage.sender!; ? newMessage.receiver!
: newMessage.sender!;
if (!partner) { if (!partner) {
console.warn('[Messages] Partner data missing from new message');
return prevConversations; return prevConversations;
} }
@@ -83,13 +83,13 @@ const Messages: React.FC = () => {
content: newMessage.content, content: newMessage.content,
senderId: newMessage.senderId, senderId: newMessage.senderId,
createdAt: newMessage.createdAt, createdAt: newMessage.createdAt,
isRead: newMessage.isRead isRead: newMessage.isRead,
}, },
unreadCount: newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0, unreadCount:
lastMessageAt: newMessage.createdAt newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
lastMessageAt: newMessage.createdAt,
}; };
console.log('[Messages] Created new conversation');
return [newConv, ...prevConversations]; return [newConv, ...prevConversations];
} }
}); });
@@ -103,16 +103,17 @@ const Messages: React.FC = () => {
if (!isConnected) return; if (!isConnected) return;
const cleanup = onMessageRead((data: any) => { const cleanup = onMessageRead((data: any) => {
console.log('[Messages] Message read:', data);
setConversations((prevConversations) => { setConversations((prevConversations) => {
return prevConversations.map(conv => { return prevConversations.map((conv) => {
// If this is the conversation and the last message was marked as read // If this is the conversation and the last message was marked as read
if (conv.lastMessage.id === data.messageId && !conv.lastMessage.isRead) { if (
conv.lastMessage.id === data.messageId &&
!conv.lastMessage.isRead
) {
return { return {
...conv, ...conv,
lastMessage: { ...conv.lastMessage, isRead: true }, lastMessage: { ...conv.lastMessage, isRead: true },
unreadCount: Math.max(0, conv.unreadCount - 1) unreadCount: Math.max(0, conv.unreadCount - 1),
}; };
} }
return conv; return conv;
@@ -127,10 +128,8 @@ const Messages: React.FC = () => {
try { try {
const response = await messageAPI.getConversations(); const response = await messageAPI.getConversations();
setConversations(response.data); setConversations(response.data);
console.log('[Messages] Fetched conversations:', response.data.length);
} catch (err: any) { } catch (err: any) {
console.error('[Messages] Failed to fetch conversations:', err); setError(err.response?.data?.error || "Failed to fetch conversations");
setError(err.response?.data?.error || 'Failed to fetch conversations');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -142,11 +141,17 @@ const Messages: React.FC = () => {
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60); const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
if (diffInHours < 24) { if (diffInHours < 24) {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
} else if (diffInHours < 48) { } else if (diffInHours < 48) {
return 'Yesterday'; return "Yesterday";
} else { } else {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
} }
}; };
@@ -156,14 +161,10 @@ const Messages: React.FC = () => {
}; };
const handleMessagesRead = (partnerId: string, count: number) => { const handleMessagesRead = (partnerId: string, count: number) => {
console.log(`[Messages] ${count} messages marked as read for partner ${partnerId}`);
// Update the conversation's unread count // Update the conversation's unread count
setConversations(prevConversations => setConversations((prevConversations) =>
prevConversations.map(conv => prevConversations.map((conv) =>
conv.partnerId === partnerId conv.partnerId === partnerId ? { ...conv, unreadCount: 0 } : conv
? { ...conv, unreadCount: 0 }
: conv
) )
); );
}; };
@@ -201,23 +202,29 @@ const Messages: React.FC = () => {
{conversations.length === 0 ? ( {conversations.length === 0 ? (
<div className="text-center py-5"> <div className="text-center py-5">
<i className="bi bi-envelope" style={{ fontSize: '3rem', color: '#ccc' }}></i> <i
className="bi bi-envelope"
style={{ fontSize: "3rem", color: "#ccc" }}
></i>
<p className="text-muted mt-2">No conversations yet</p> <p className="text-muted mt-2">No conversations yet</p>
</div> </div>
) : ( ) : (
<div className="list-group"> <div className="list-group">
{conversations.map((conversation) => { {conversations.map((conversation) => {
const isUnread = conversation.unreadCount > 0; const isUnread = conversation.unreadCount > 0;
const isLastMessageFromPartner = conversation.lastMessage.senderId === conversation.partnerId; const isLastMessageFromPartner =
conversation.lastMessage.senderId === conversation.partnerId;
return ( return (
<div <div
key={conversation.partnerId} key={conversation.partnerId}
className={`list-group-item list-group-item-action ${isUnread ? 'border-start border-primary border-4' : ''}`} className={`list-group-item list-group-item-action ${
isUnread ? "border-start border-primary border-4" : ""
}`}
onClick={() => handleConversationClick(conversation)} onClick={() => handleConversationClick(conversation)}
style={{ style={{
cursor: 'pointer', cursor: "pointer",
backgroundColor: isUnread ? '#f0f7ff' : 'white' backgroundColor: isUnread ? "#f0f7ff" : "white",
}} }}
> >
<div className="d-flex w-100 justify-content-between align-items-start"> <div className="d-flex w-100 justify-content-between align-items-start">
@@ -228,12 +235,16 @@ const Messages: React.FC = () => {
src={conversation.partner.profileImage} src={conversation.partner.profileImage}
alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`} alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`}
className="rounded-circle me-3" className="rounded-circle me-3"
style={{ width: '50px', height: '50px', objectFit: 'cover' }} style={{
width: "50px",
height: "50px",
objectFit: "cover",
}}
/> />
) : ( ) : (
<div <div
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3" className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3"
style={{ width: '50px', height: '50px' }} style={{ width: "50px", height: "50px" }}
> >
<i className="bi bi-person-fill text-white"></i> <i className="bi bi-person-fill text-white"></i>
</div> </div>
@@ -242,18 +253,25 @@ const Messages: React.FC = () => {
<div className="flex-grow-1" style={{ minWidth: 0 }}> <div className="flex-grow-1" style={{ minWidth: 0 }}>
{/* User Name and Unread Badge */} {/* User Name and Unread Badge */}
<div className="d-flex align-items-center mb-1"> <div className="d-flex align-items-center mb-1">
<h6 className={`mb-0 ${isUnread ? 'fw-bold' : ''}`}> <h6 className={`mb-0 ${isUnread ? "fw-bold" : ""}`}>
{conversation.partner.firstName} {conversation.partner.lastName} {conversation.partner.firstName}{" "}
{conversation.partner.lastName}
</h6> </h6>
{isUnread && ( {isUnread && (
<span className="badge bg-primary ms-2">{conversation.unreadCount}</span> <span className="badge bg-primary ms-2">
{conversation.unreadCount}
</span>
)} )}
</div> </div>
{/* Last Message Preview */} {/* Last Message Preview */}
<p <p
className={`mb-0 text-truncate ${isUnread && isLastMessageFromPartner ? 'fw-semibold' : 'text-muted'}`} className={`mb-0 text-truncate ${
style={{ maxWidth: '100%' }} isUnread && isLastMessageFromPartner
? "fw-semibold"
: "text-muted"
}`}
style={{ maxWidth: "100%" }}
> >
{conversation.lastMessage.senderId === user?.id && ( {conversation.lastMessage.senderId === user?.id && (
<span className="me-1">You: </span> <span className="me-1">You: </span>
@@ -264,7 +282,10 @@ const Messages: React.FC = () => {
</div> </div>
{/* Timestamp */} {/* Timestamp */}
<div className="text-end ms-3" style={{ minWidth: 'fit-content' }}> <div
className="text-end ms-3"
style={{ minWidth: "fit-content" }}
>
<small className="text-muted d-block"> <small className="text-muted d-block">
{formatDate(conversation.lastMessageAt)} {formatDate(conversation.lastMessageAt)}
</small> </small>

View File

@@ -39,7 +39,7 @@ const api = axios.create({
export const fetchCSRFToken = async (): Promise<string> => { export const fetchCSRFToken = async (): Promise<string> => {
try { try {
const response = await api.get("/auth/csrf-token"); const response = await api.get("/auth/csrf-token");
csrfToken = response.data.csrfToken || ""; csrfToken = response.headers["x-csrf-token"] || "";
return csrfToken || ""; return csrfToken || "";
} catch (error) { } catch (error) {
console.error("Failed to fetch CSRF token:", error); console.error("Failed to fetch CSRF token:", error);
@@ -286,10 +286,14 @@ export const forumAPI = {
deleteComment: (commentId: string) => deleteComment: (commentId: string) =>
api.delete(`/forum/comments/${commentId}`), api.delete(`/forum/comments/${commentId}`),
// Admin endpoints // Admin endpoints
adminDeletePost: (id: string, reason: string) => api.delete(`/forum/admin/posts/${id}`, { data: { reason } }), adminDeletePost: (id: string, reason: string) =>
adminRestorePost: (id: string) => api.patch(`/forum/admin/posts/${id}/restore`), api.delete(`/forum/admin/posts/${id}`, { data: { reason } }),
adminDeleteComment: (id: string, reason: string) => api.delete(`/forum/admin/comments/${id}`, { data: { reason } }), adminRestorePost: (id: string) =>
adminRestoreComment: (id: string) => api.patch(`/forum/admin/comments/${id}/restore`), api.patch(`/forum/admin/posts/${id}/restore`),
adminDeleteComment: (id: string, reason: string) =>
api.delete(`/forum/admin/comments/${id}`, { data: { reason } }),
adminRestoreComment: (id: string) =>
api.patch(`/forum/admin/comments/${id}/restore`),
adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`), adminClosePost: (id: string) => api.patch(`/forum/admin/posts/${id}/close`),
adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`), adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`),
}; };

View File

@@ -52,12 +52,9 @@ class SocketService {
*/ */
connect(): Socket { connect(): Socket {
if (this.socket?.connected) { if (this.socket?.connected) {
console.log("[Socket] Already connected");
return this.socket; return this.socket;
} }
console.log("[Socket] Connecting to server...");
this.socket = io(this.getSocketUrl(), { this.socket = io(this.getSocketUrl(), {
withCredentials: true, // Send cookies for authentication withCredentials: true, // Send cookies for authentication
transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling
@@ -71,23 +68,15 @@ class SocketService {
// Connection event handlers // Connection event handlers
this.socket.on("connect", () => { this.socket.on("connect", () => {
console.log("[Socket] Connected successfully", {
socketId: this.socket?.id,
});
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.notifyConnectionListeners(true); this.notifyConnectionListeners(true);
}); });
this.socket.on("disconnect", (reason) => { this.socket.on("disconnect", (reason) => {
console.log("[Socket] Disconnected", { reason });
this.notifyConnectionListeners(false); this.notifyConnectionListeners(false);
}); });
this.socket.on("connect_error", (error) => { this.socket.on("connect_error", (error) => {
console.error("[Socket] Connection error", {
error: error.message,
attempt: this.reconnectAttempts + 1,
});
this.reconnectAttempts++; this.reconnectAttempts++;
if (this.reconnectAttempts >= this.maxReconnectAttempts) { if (this.reconnectAttempts >= this.maxReconnectAttempts) {
@@ -96,10 +85,6 @@ class SocketService {
} }
}); });
this.socket.on("error", (error) => {
console.error("[Socket] Socket error", error);
});
return this.socket; return this.socket;
} }
@@ -108,7 +93,6 @@ class SocketService {
*/ */
disconnect(): void { disconnect(): void {
if (this.socket) { if (this.socket) {
console.log("[Socket] Disconnecting...");
this.socket.disconnect(); this.socket.disconnect();
this.socket = null; this.socket = null;
this.notifyConnectionListeners(false); this.notifyConnectionListeners(false);
@@ -138,7 +122,6 @@ class SocketService {
return; return;
} }
console.log("[Socket] Joining conversation", { otherUserId });
this.socket.emit("join_conversation", { otherUserId }); this.socket.emit("join_conversation", { otherUserId });
} }
@@ -150,7 +133,6 @@ class SocketService {
return; return;
} }
console.log("[Socket] Leaving conversation", { otherUserId });
this.socket.emit("leave_conversation", { otherUserId }); this.socket.emit("leave_conversation", { otherUserId });
} }