Compare commits
3 Commits
f3a356d64b
...
f2d3aac029
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2d3aac029 | ||
|
|
fab79e64ee | ||
|
|
8b10103ae4 |
@@ -14,7 +14,7 @@ const authenticateToken = async (req, res, next) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
|
||||
const userId = decoded.id;
|
||||
|
||||
if (!userId) {
|
||||
@@ -78,7 +78,7 @@ const optionalAuth = async (req, res, next) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
|
||||
const userId = decoded.id;
|
||||
|
||||
if (!userId) {
|
||||
|
||||
@@ -72,7 +72,8 @@ const getCSRFToken = (req, res) => {
|
||||
maxAge: 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
res.json({ csrfToken: token });
|
||||
res.set("X-CSRF-Token", token);
|
||||
res.status(204).send();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
145
backend/package-lock.json
generated
145
backend/package-lock.json
generated
@@ -4067,22 +4067,43 @@
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
||||
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"type-is": "^2.0.0"
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
@@ -4181,6 +4202,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -4745,9 +4767,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -5359,27 +5382,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-validator": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
|
||||
"integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
|
||||
"integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"validator": "~13.12.0"
|
||||
"validator": "~13.15.23"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -5789,9 +5803,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
@@ -6006,26 +6021,23 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
@@ -7145,9 +7157,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8454,17 +8466,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
|
||||
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.6.3",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"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": {
|
||||
@@ -9812,6 +9841,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -9917,9 +9947,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||
"version": "13.15.23",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz",
|
||||
"integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
|
||||
@@ -128,13 +128,13 @@ router.post(
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion },
|
||||
process.env.JWT_SECRET,
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: "15m" } // Short-lived access token
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
|
||||
process.env.JWT_SECRET,
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
|
||||
@@ -223,13 +223,13 @@ router.post(
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion },
|
||||
process.env.JWT_SECRET,
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: "15m" } // Short-lived access token
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
|
||||
process.env.JWT_SECRET,
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
|
||||
@@ -392,13 +392,13 @@ router.post(
|
||||
// Generate JWT tokens
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion },
|
||||
process.env.JWT_SECRET,
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: "15m" }
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
|
||||
process.env.JWT_SECRET,
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ 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);
|
||||
|
||||
if (!user) {
|
||||
@@ -625,7 +625,7 @@ router.post("/refresh", async (req, res) => {
|
||||
}
|
||||
|
||||
// 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") {
|
||||
return res.status(401).json({ error: "Invalid refresh token" });
|
||||
@@ -648,7 +648,7 @@ router.post("/refresh", async (req, res) => {
|
||||
// Generate new access token
|
||||
const newAccessToken = jwt.sign(
|
||||
{ id: user.id, jwtVersion: user.jwtVersion },
|
||||
process.env.JWT_SECRET,
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: "15m" }
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const emailServices = require('../services/email');
|
||||
const router = express.Router();
|
||||
|
||||
// Submit new feedback
|
||||
router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, res) => {
|
||||
router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req, res, next) => {
|
||||
try {
|
||||
const { feedbackText, url } = req.body;
|
||||
|
||||
@@ -59,7 +59,7 @@ router.post('/', authenticateToken, sanitizeInput, validateFeedback, async (req,
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const buildCommentTree = (comments, isAdmin = false) => {
|
||||
};
|
||||
|
||||
// GET /api/forum/posts - Browse all posts
|
||||
router.get('/posts', optionalAuth, async (req, res) => {
|
||||
router.get('/posts', optionalAuth, async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
search,
|
||||
@@ -158,12 +158,12 @@ router.get('/posts', optionalAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
query: req.query
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const post = await ForumPost.findByPk(req.params.id, {
|
||||
include: [
|
||||
@@ -233,12 +233,12 @@ router.get('/posts/:id', optionalAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
postId: req.params.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
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,
|
||||
postData: logger.sanitize(req.body)
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
@@ -549,12 +549,12 @@ router.put('/posts/:id', authenticateToken, async (req, res) => {
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
|
||||
@@ -586,12 +586,12 @@ router.delete('/posts/:id', authenticateToken, async (req, res) => {
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const { status } = req.body;
|
||||
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,
|
||||
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
|
||||
router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) => {
|
||||
router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const { commentId } = req.body;
|
||||
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,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const { content, parentCommentId } = req.body;
|
||||
const post = await ForumPost.findByPk(req.params.id);
|
||||
@@ -1073,12 +1073,12 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
|
||||
postId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const { content } = req.body;
|
||||
const comment = await ForumComment.findByPk(req.params.id);
|
||||
@@ -1122,12 +1122,12 @@ router.put('/comments/:id', authenticateToken, async (req, res) => {
|
||||
commentId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const comment = await ForumComment.findByPk(req.params.id);
|
||||
|
||||
@@ -1164,12 +1164,12 @@ router.delete('/comments/:id', authenticateToken, async (req, res) => {
|
||||
commentId: req.params.id,
|
||||
authorId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const posts = await ForumPost.findAll({
|
||||
where: { authorId: req.user.id },
|
||||
@@ -1202,12 +1202,12 @@ router.get('/my-posts', authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/forum/tags - Get all unique tags for autocomplete
|
||||
router.get('/tags', async (req, res) => {
|
||||
router.get('/tags', async (req, res, next) => {
|
||||
try {
|
||||
const { search } = req.query;
|
||||
|
||||
@@ -1241,14 +1241,14 @@ router.get('/tags', async (req, res) => {
|
||||
stack: error.stack,
|
||||
query: req.query
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// ============ ADMIN ROUTES ============
|
||||
|
||||
// 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 {
|
||||
const { reason } = req.body;
|
||||
|
||||
@@ -1321,12 +1321,12 @@ router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, r
|
||||
postId: req.params.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
|
||||
router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async (req, res) => {
|
||||
router.patch('/admin/posts/:id/restore', authenticateToken, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
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,
|
||||
adminId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const { reason } = req.body;
|
||||
|
||||
@@ -1449,12 +1449,12 @@ router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req
|
||||
commentId: req.params.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
|
||||
router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, async (req, res) => {
|
||||
router.patch('/admin/comments/:id/restore', authenticateToken, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
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,
|
||||
adminId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const post = await ForumPost.findByPk(req.params.id, {
|
||||
include: [
|
||||
@@ -1615,12 +1615,12 @@ router.patch('/admin/posts/:id/close', authenticateToken, requireAdmin, async (r
|
||||
postId: req.params.id,
|
||||
adminId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
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,
|
||||
adminId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const { authenticateToken, requireVerifiedEmail, requireAdmin, optionalAuth } =
|
||||
const logger = require("../utils/logger");
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
router.get("/", async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
minPrice,
|
||||
@@ -84,11 +84,11 @@ router.get("/", async (req, res) => {
|
||||
stack: error.stack,
|
||||
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 {
|
||||
const userRentals = await Rental.findAll({
|
||||
where: { renterId: req.user.id },
|
||||
@@ -119,12 +119,12 @@ router.get("/recommendations", authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
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)
|
||||
router.get('/:id/reviews', async (req, res) => {
|
||||
router.get('/:id/reviews', async (req, res, next) => {
|
||||
try {
|
||||
const { Rental, User } = require('../models');
|
||||
|
||||
@@ -169,11 +169,11 @@ router.get('/:id/reviews', async (req, res) => {
|
||||
stack: error.stack,
|
||||
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 {
|
||||
const item = await Item.findByPk(req.params.id, {
|
||||
include: [
|
||||
@@ -226,11 +226,11 @@ router.get("/:id", optionalAuth, async (req, res) => {
|
||||
stack: error.stack,
|
||||
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 {
|
||||
const item = await Item.create({
|
||||
...req.body,
|
||||
@@ -284,11 +284,11 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
||||
ownerId: req.user.id,
|
||||
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 {
|
||||
const item = await Item.findByPk(req.params.id);
|
||||
|
||||
@@ -327,11 +327,11 @@ router.put("/:id", authenticateToken, async (req, res) => {
|
||||
itemId: req.params.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 {
|
||||
const item = await Item.findByPk(req.params.id);
|
||||
|
||||
@@ -360,12 +360,12 @@ router.delete("/:id", authenticateToken, async (req, res) => {
|
||||
itemId: req.params.id,
|
||||
ownerId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Admin endpoints
|
||||
router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res) => {
|
||||
router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { reason } = req.body;
|
||||
|
||||
@@ -463,11 +463,11 @@ router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res) =>
|
||||
itemId: req.params.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 {
|
||||
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,
|
||||
adminId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const path = require('path');
|
||||
const router = express.Router();
|
||||
|
||||
// Get all messages for the current user (inbox)
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
router.get('/', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const messages = await Message.findAll({
|
||||
where: { receiverId: req.user.id },
|
||||
@@ -40,12 +40,12 @@ router.get('/', authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get conversations grouped by user pairs
|
||||
router.get('/conversations', authenticateToken, async (req, res) => {
|
||||
router.get('/conversations', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
@@ -134,12 +134,12 @@ router.get('/conversations', authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get sent messages
|
||||
router.get('/sent', authenticateToken, async (req, res) => {
|
||||
router.get('/sent', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const messages = await Message.findAll({
|
||||
where: { senderId: req.user.id },
|
||||
@@ -167,12 +167,12 @@ router.get('/sent', authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get a single message
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
router.get('/:id', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const message = await Message.findOne({
|
||||
where: {
|
||||
@@ -232,12 +232,12 @@ router.get('/:id', authenticateToken, async (req, res) => {
|
||||
userId: req.user.id,
|
||||
messageId: req.params.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Send a new message
|
||||
router.post('/', authenticateToken, uploadMessageImage, async (req, res) => {
|
||||
router.post('/', authenticateToken, uploadMessageImage, async (req, res, next) => {
|
||||
try {
|
||||
const { receiverId, content } = req.body;
|
||||
|
||||
@@ -309,12 +309,12 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res) => {
|
||||
senderId: req.user.id,
|
||||
receiverId: req.body.receiverId
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Mark message as read
|
||||
router.put('/:id/read', authenticateToken, async (req, res) => {
|
||||
router.put('/:id/read', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const message = await Message.findOne({
|
||||
where: {
|
||||
@@ -354,12 +354,12 @@ router.put('/:id/read', authenticateToken, async (req, res) => {
|
||||
userId: req.user.id,
|
||||
messageId: req.params.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get unread message count
|
||||
router.get('/unread/count', authenticateToken, async (req, res) => {
|
||||
router.get('/unread/count', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const count = await Message.count({
|
||||
where: {
|
||||
@@ -381,7 +381,7 @@ router.get('/unread/count', authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
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" }),
|
||||
async (req, res) => {
|
||||
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
|
||||
const message = await Message.findOne({
|
||||
|
||||
@@ -932,7 +932,7 @@ router.post("/cost-preview", authenticateToken, async (req, res) => {
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const ownerRentals = await Rental.findAll({
|
||||
where: {
|
||||
@@ -960,12 +960,12 @@ router.get("/earnings/status", authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const preview = await RefundService.getRefundPreview(
|
||||
req.params.id,
|
||||
@@ -980,12 +980,12 @@ router.get("/:id/refund-preview", authenticateToken, async (req, res) => {
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(400).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const { actualReturnDateTime } = req.query;
|
||||
|
||||
@@ -1020,12 +1020,12 @@ router.get("/:id/late-fee-preview", authenticateToken, async (req, res) => {
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(400).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel rental with refund processing
|
||||
router.post("/:id/cancel", authenticateToken, async (req, res) => {
|
||||
router.post("/:id/cancel", authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const { reason } = req.body;
|
||||
|
||||
@@ -1092,12 +1092,12 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
|
||||
rentalId: req.params.id,
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(400).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const { status, actualReturnDateTime, statusOptions } = req.body;
|
||||
const rentalId = req.params.id;
|
||||
@@ -1253,12 +1253,12 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
|
||||
userId: req.user.id,
|
||||
});
|
||||
|
||||
res.status(400).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const rentalId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
@@ -1290,7 +1290,7 @@ router.post("/:id/report-damage", authenticateToken, async (req, res) => {
|
||||
userId: req.user.id,
|
||||
});
|
||||
|
||||
res.status(400).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const logger = require("../utils/logger");
|
||||
const router = express.Router();
|
||||
|
||||
// Get checkout session status
|
||||
router.get("/checkout-session/:sessionId", async (req, res) => {
|
||||
router.get("/checkout-session/:sessionId", async (req, res, next) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
@@ -32,14 +32,14 @@ router.get("/checkout-session/:sessionId", async (req, res) => {
|
||||
reqLogger.error("Stripe checkout session retrieval failed", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
sessionId: sessionId,
|
||||
sessionId: req.params.sessionId,
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Create connected account
|
||||
router.post("/accounts", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
||||
router.post("/accounts", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
@@ -82,12 +82,12 @@ router.post("/accounts", authenticateToken, requireVerifiedEmail, async (req, re
|
||||
stack: error.stack,
|
||||
userId: req.user.id,
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate onboarding link
|
||||
router.post("/account-links", authenticateToken, requireVerifiedEmail, async (req, res) => {
|
||||
router.post("/account-links", authenticateToken, requireVerifiedEmail, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
@@ -128,12 +128,12 @@ router.post("/account-links", authenticateToken, requireVerifiedEmail, async (re
|
||||
userId: req.user.id,
|
||||
stripeConnectedAccountId: user?.stripeConnectedAccountId,
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get account status
|
||||
router.get("/account-status", authenticateToken, async (req, res) => {
|
||||
router.get("/account-status", authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
@@ -168,7 +168,7 @@ router.get("/account-status", authenticateToken, async (req, res) => {
|
||||
userId: req.user.id,
|
||||
stripeConnectedAccountId: user?.stripeConnectedAccountId,
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ router.post(
|
||||
"/create-setup-checkout-session",
|
||||
authenticateToken,
|
||||
requireVerifiedEmail,
|
||||
async (req, res) => {
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { rentalData } = req.body;
|
||||
|
||||
@@ -238,7 +238,7 @@ router.post(
|
||||
userId: req.user.id,
|
||||
stripeCustomerId: user?.stripeCustomerId,
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/profile', authenticateToken, async (req, res) => {
|
||||
router.get('/profile', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id, {
|
||||
attributes: { exclude: ['password'] }
|
||||
@@ -27,12 +27,12 @@ router.get('/profile', authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.status(500).json({ error: error.message });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Address routes (must come before /:id route)
|
||||
router.get('/addresses', authenticateToken, async (req, res) => {
|
||||
router.get('/addresses', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const addresses = await UserAddress.findAll({
|
||||
where: { userId: req.user.id },
|
||||
@@ -52,11 +52,11 @@ router.get('/addresses', authenticateToken, async (req, res) => {
|
||||
stack: error.stack,
|
||||
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 {
|
||||
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,
|
||||
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 {
|
||||
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') {
|
||||
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 {
|
||||
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') {
|
||||
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)
|
||||
router.get('/availability', authenticateToken, async (req, res) => {
|
||||
router.get('/availability', authenticateToken, async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id, {
|
||||
attributes: ['defaultAvailableAfter', 'defaultAvailableBefore', 'defaultSpecifyTimesPerDay', 'defaultWeeklyTimes']
|
||||
@@ -130,11 +130,11 @@ router.get('/availability', authenticateToken, async (req, res) => {
|
||||
weeklyTimes: user.defaultWeeklyTimes
|
||||
});
|
||||
} 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 {
|
||||
const { generalAvailableAfter, generalAvailableBefore, specifyTimesPerDay, weeklyTimes } = req.body;
|
||||
|
||||
@@ -149,11 +149,11 @@ router.put('/availability', authenticateToken, async (req, res) => {
|
||||
|
||||
res.json({ message: 'Availability updated successfully' });
|
||||
} 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 {
|
||||
const user = await User.findByPk(req.params.id, {
|
||||
attributes: { exclude: ['password', 'email', 'phone', 'address'] }
|
||||
@@ -176,11 +176,11 @@ router.get('/:id', async (req, res) => {
|
||||
stack: error.stack,
|
||||
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 {
|
||||
// Use UserService to handle update and email notification
|
||||
const updatedUser = await userService.updateProfile(req.user.id, req.body);
|
||||
@@ -188,10 +188,7 @@ router.put('/profile', authenticateToken, async (req, res) => {
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error('Profile update error:', error);
|
||||
res.status(500).json({
|
||||
error: error.message,
|
||||
details: error.errors ? error.errors.map(e => ({ field: e.path, message: e.message })) : undefined
|
||||
});
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ app.use(
|
||||
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200,
|
||||
exposedHeaders: ["X-CSRF-Token"],
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { sequelize } = require('../models');
|
||||
const { QueryTypes } = require('sequelize');
|
||||
const { sequelize } = require("../models");
|
||||
const { QueryTypes } = require("sequelize");
|
||||
|
||||
class LocationService {
|
||||
/**
|
||||
@@ -13,19 +13,13 @@ class LocationService {
|
||||
*/
|
||||
async findUsersInRadius(latitude, longitude, radiusMiles = 10) {
|
||||
if (!latitude || !longitude) {
|
||||
throw new Error('Latitude and longitude are required');
|
||||
throw new Error("Latitude and longitude are required");
|
||||
}
|
||||
|
||||
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 {
|
||||
// Haversine formula:
|
||||
// distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2))
|
||||
@@ -62,29 +56,22 @@ class LocationService {
|
||||
replacements: {
|
||||
lat: parseFloat(latitude),
|
||||
lng: parseFloat(longitude),
|
||||
radiusMiles: parseFloat(radiusMiles)
|
||||
radiusMiles: parseFloat(radiusMiles),
|
||||
},
|
||||
type: QueryTypes.SELECT
|
||||
type: QueryTypes.SELECT,
|
||||
});
|
||||
|
||||
console.log('Users found in radius:', users.map(u => ({
|
||||
id: u.id,
|
||||
userLat: u.latitude,
|
||||
userLng: u.longitude,
|
||||
distance: parseFloat(u.distance).toFixed(2)
|
||||
})));
|
||||
|
||||
return users.map(user => ({
|
||||
return users.map((user) => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
latitude: parseFloat(user.latitude),
|
||||
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) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -105,8 +92,10 @@ class LocationService {
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
Math.cos(this.toRadians(lat1)) *
|
||||
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 distance = R * c;
|
||||
|
||||
@@ -38,8 +38,8 @@ const authenticateSocket = async (socket, next) => {
|
||||
return next(new Error("Authentication required"));
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
// Verify JWT (access tokens only)
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
|
||||
const userId = decoded.id;
|
||||
|
||||
if (!userId) {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import React, { useState, useEffect, useLayoutEffect, useRef, 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';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
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 {
|
||||
show: boolean;
|
||||
@@ -12,15 +18,30 @@ interface ChatWindowProps {
|
||||
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 { isConnected, joinConversation, leaveConversation, onNewMessage, onUserTyping, emitTypingStart, emitTypingStop } = useSocket();
|
||||
const {
|
||||
isConnected,
|
||||
joinConversation,
|
||||
leaveConversation,
|
||||
onNewMessage,
|
||||
onUserTyping,
|
||||
emitTypingStart,
|
||||
emitTypingStop,
|
||||
} = useSocket();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [newMessage, setNewMessage] = useState("");
|
||||
const [sending, setSending] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 [hasScrolledToUnread, setHasScrolledToUnread] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
||||
@@ -52,28 +73,29 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
}, [show, recipient.id, isConnected]);
|
||||
|
||||
// Create a stable callback for handling new messages
|
||||
const handleNewMessage = useCallback(async (message: Message) => {
|
||||
console.log('[ChatWindow] Received new_message event:', message);
|
||||
|
||||
const handleNewMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
// Only add messages that are part of this conversation
|
||||
if (
|
||||
(message.senderId === recipient.id && message.receiverId === currentUser?.id) ||
|
||||
(message.senderId === currentUser?.id && message.receiverId === recipient.id)
|
||||
(message.senderId === recipient.id &&
|
||||
message.receiverId === currentUser?.id) ||
|
||||
(message.senderId === currentUser?.id &&
|
||||
message.receiverId === recipient.id)
|
||||
) {
|
||||
console.log('[ChatWindow] Message is for this conversation, adding to chat');
|
||||
setMessages((prevMessages) => {
|
||||
// Check if message already exists (avoid duplicates)
|
||||
if (prevMessages.some(m => m.id === message.id)) {
|
||||
console.log('[ChatWindow] Message already exists, skipping');
|
||||
if (prevMessages.some((m) => m.id === message.id)) {
|
||||
return prevMessages;
|
||||
}
|
||||
console.log('[ChatWindow] Adding new message to chat');
|
||||
return [...prevMessages, message];
|
||||
});
|
||||
|
||||
// Mark incoming messages from recipient as read
|
||||
if (message.senderId === recipient.id && message.receiverId === currentUser?.id && !message.isRead) {
|
||||
console.log('[ChatWindow] Marking new incoming message as read');
|
||||
if (
|
||||
message.senderId === recipient.id &&
|
||||
message.receiverId === currentUser?.id &&
|
||||
!message.isRead
|
||||
) {
|
||||
try {
|
||||
await messageAPI.markAsRead(message.id);
|
||||
|
||||
@@ -82,29 +104,26 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
onMessagesRead(recipient.id, 1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to mark message ${message.id} as read:`, 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
|
||||
useEffect(() => {
|
||||
console.log('[ChatWindow] Message listener useEffect running', { isConnected, show, recipientId: recipient.id });
|
||||
|
||||
if (!isConnected || !show) {
|
||||
console.log('[ChatWindow] Skipping listener setup:', { isConnected, show });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ChatWindow] Setting up message listener for recipient:', recipient.id);
|
||||
|
||||
const cleanup = onNewMessage(handleNewMessage);
|
||||
|
||||
return () => {
|
||||
console.log('[ChatWindow] Cleaning up message listener for recipient:', recipient.id);
|
||||
cleanup();
|
||||
};
|
||||
}, [isConnected, show, onNewMessage, handleNewMessage]);
|
||||
@@ -143,21 +162,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
if (!loading && !hasScrolledToUnread && messages.length > 0) {
|
||||
if (initialUnreadMessageIds.size > 0) {
|
||||
// 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)) {
|
||||
console.log(`[ChatWindow] Scrolling to oldest unread message: ${oldestUnread.id}`);
|
||||
messageRefs.current.get(oldestUnread.id)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
} else {
|
||||
console.log('[ChatWindow] Unread message ref not found, scrolling to bottom');
|
||||
scrollToBottom();
|
||||
}
|
||||
} else {
|
||||
// No unread messages, scroll to bottom
|
||||
console.log('[ChatWindow] No unread messages, scrolling to bottom');
|
||||
scrollToBottom();
|
||||
}
|
||||
setHasScrolledToUnread(true);
|
||||
@@ -176,26 +194,33 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
// Fetch all messages between current user and recipient
|
||||
const [sentRes, receivedRes] = await Promise.all([
|
||||
messageAPI.getSentMessages(),
|
||||
messageAPI.getMessages()
|
||||
messageAPI.getMessages(),
|
||||
]);
|
||||
|
||||
const sentToRecipient = sentRes.data.filter((msg: Message) => msg.receiverId === recipient.id);
|
||||
const receivedFromRecipient = receivedRes.data.filter((msg: Message) => msg.senderId === recipient.id);
|
||||
const sentToRecipient = sentRes.data.filter(
|
||||
(msg: Message) => msg.receiverId === recipient.id
|
||||
);
|
||||
const receivedFromRecipient = receivedRes.data.filter(
|
||||
(msg: Message) => msg.senderId === recipient.id
|
||||
);
|
||||
|
||||
// Combine and sort by date
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
console.log(`[ChatWindow] Marking ${unreadMessages.length} messages as read`);
|
||||
|
||||
// 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
|
||||
const markReadPromises = unreadMessages.map((message: Message) =>
|
||||
@@ -212,28 +237,32 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch messages:', error);
|
||||
console.error("Failed to fetch messages:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const setMessageRef = useCallback((id: string) => (el: HTMLDivElement | null) => {
|
||||
const setMessageRef = useCallback(
|
||||
(id: string) => (el: HTMLDivElement | null) => {
|
||||
if (el) {
|
||||
messageRefs.current.set(id, el);
|
||||
} else {
|
||||
messageRefs.current.delete(id);
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!messagesContainerRef.current) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current;
|
||||
const { scrollTop, scrollHeight, clientHeight } =
|
||||
messagesContainerRef.current;
|
||||
const isBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold
|
||||
setIsAtBottom(isBottom);
|
||||
};
|
||||
@@ -265,14 +294,14 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Please select an image file');
|
||||
if (!file.type.startsWith("image/")) {
|
||||
alert("Please select an image file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert('Image size must be less than 5MB');
|
||||
alert("Image size must be less than 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -291,7 +320,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
setSelectedImage(null);
|
||||
setImagePreview(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -311,20 +340,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
setSending(true);
|
||||
const messageContent = newMessage;
|
||||
const imageToSend = selectedImage;
|
||||
setNewMessage(''); // Clear input immediately for better UX
|
||||
setNewMessage(""); // Clear input immediately for better UX
|
||||
setSelectedImage(null);
|
||||
setImagePreview(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
|
||||
try {
|
||||
// Build FormData for message (with or without image)
|
||||
const formData = new FormData();
|
||||
formData.append('receiverId', recipient.id);
|
||||
formData.append('content', messageContent || ' '); // Send space if only image
|
||||
formData.append("receiverId", recipient.id);
|
||||
formData.append("content", messageContent || " "); // Send space if only image
|
||||
if (imageToSend) {
|
||||
formData.append('image', imageToSend);
|
||||
formData.append("image", imageToSend);
|
||||
}
|
||||
|
||||
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
|
||||
setMessages((prevMessages) => {
|
||||
// Avoid duplicates
|
||||
if (prevMessages.some(m => m.id === response.data.id)) {
|
||||
if (prevMessages.some((m) => m.id === response.data.id)) {
|
||||
return prevMessages;
|
||||
}
|
||||
return [...prevMessages, response.data];
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
console.error("Failed to send message:", error);
|
||||
setNewMessage(messageContent); // Restore message on error
|
||||
setSelectedImage(imageToSend);
|
||||
if (imageToSend) {
|
||||
@@ -362,10 +391,10 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
|
||||
const formatTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -374,19 +403,19 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
const today = new Date();
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return 'Today';
|
||||
return "Today";
|
||||
}
|
||||
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
if (date.toDateString() === yesterday.toDateString()) {
|
||||
return 'Yesterday';
|
||||
return "Yesterday";
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: date.getFullYear() !== today.getFullYear() ? "numeric" : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -396,13 +425,13 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
<div
|
||||
className="position-fixed bottom-0 end-0 m-3 shadow-lg d-flex flex-column"
|
||||
style={{
|
||||
width: '350px',
|
||||
height: '500px',
|
||||
maxHeight: 'calc(100vh - 100px)',
|
||||
width: "350px",
|
||||
height: "500px",
|
||||
maxHeight: "calc(100vh - 100px)",
|
||||
zIndex: 1050,
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'white'
|
||||
borderRadius: "12px",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
@@ -413,18 +442,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
src={recipient.profileImage}
|
||||
alt={`${recipient.firstName} ${recipient.lastName}`}
|
||||
className="rounded-circle me-2"
|
||||
style={{ width: '35px', height: '35px', objectFit: 'cover' }}
|
||||
style={{ width: "35px", height: "35px", objectFit: "cover" }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h6 className="mb-0">{recipient.firstName} {recipient.lastName}</h6>
|
||||
<h6 className="mb-0">
|
||||
{recipient.firstName} {recipient.lastName}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -440,8 +471,8 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
onScroll={handleScroll}
|
||||
className="p-3 overflow-auto flex-grow-1"
|
||||
style={{
|
||||
backgroundColor: '#f8f9fa',
|
||||
minHeight: 0
|
||||
backgroundColor: "#f8f9fa",
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
@@ -452,15 +483,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<i className="bi bi-chat-dots" style={{ fontSize: '3rem', color: '#dee2e6' }}></i>
|
||||
<p className="text-muted mt-2">Start a conversation with {recipient.firstName}</p>
|
||||
<i
|
||||
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>
|
||||
) : (
|
||||
<>
|
||||
{messages.map((message, index) => {
|
||||
const isCurrentUser = message.senderId === currentUser?.id;
|
||||
const showDate = index === 0 ||
|
||||
formatDate(message.createdAt) !== formatDate(messages[index - 1].createdAt);
|
||||
const showDate =
|
||||
index === 0 ||
|
||||
formatDate(message.createdAt) !==
|
||||
formatDate(messages[index - 1].createdAt);
|
||||
|
||||
return (
|
||||
<div key={message.id} ref={setMessageRef(message.id)}>
|
||||
@@ -471,16 +509,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
<div className={`d-flex mb-2 ${isCurrentUser ? 'justify-content-end' : ''}`}>
|
||||
<div
|
||||
className={`d-flex mb-2 ${
|
||||
isCurrentUser ? "justify-content-end" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`px-3 py-2 rounded-3 ${
|
||||
isCurrentUser
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-white border'
|
||||
? "bg-primary text-white"
|
||||
: "bg-white border"
|
||||
}`}
|
||||
style={{
|
||||
maxWidth: '75%',
|
||||
wordBreak: 'break-word'
|
||||
maxWidth: "75%",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{message.imagePath && (
|
||||
@@ -489,24 +531,29 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
src={getMessageImageUrl(message.imagePath)}
|
||||
alt="Shared image"
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
maxHeight: '300px',
|
||||
objectFit: 'cover'
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
maxHeight: "300px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
onClick={() => window.open(getMessageImageUrl(message.imagePath!), '_blank')}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getMessageImageUrl(message.imagePath!),
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{message.content.trim() && (
|
||||
<p className="mb-1" style={{ fontSize: '0.95rem' }}>
|
||||
<p className="mb-1" style={{ fontSize: "0.95rem" }}>
|
||||
{message.content}
|
||||
</p>
|
||||
)}
|
||||
<small
|
||||
className={isCurrentUser ? 'opacity-75' : 'text-muted'}
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
className={isCurrentUser ? "opacity-75" : "text-muted"}
|
||||
style={{ fontSize: "0.75rem" }}
|
||||
>
|
||||
{formatTime(message.createdAt)}
|
||||
</small>
|
||||
@@ -536,17 +583,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '150px',
|
||||
maxHeight: '150px',
|
||||
borderRadius: '8px',
|
||||
objectFit: 'cover'
|
||||
maxWidth: "150px",
|
||||
maxHeight: "150px",
|
||||
borderRadius: "8px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 rounded-circle"
|
||||
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>
|
||||
</button>
|
||||
@@ -558,7 +610,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageSelect}
|
||||
style={{ display: 'none' }}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -584,7 +636,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
disabled={sending || (!newMessage.trim() && !selectedImage)}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import socketService from '../services/socket';
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { Socket } from "socket.io-client";
|
||||
import socketService from "../services/socket";
|
||||
|
||||
/**
|
||||
* Socket Context Type
|
||||
@@ -14,8 +21,20 @@ interface SocketContextType {
|
||||
emitTypingStop: (receiverId: string) => void;
|
||||
emitMarkMessageRead: (messageId: string, senderId: string) => void;
|
||||
onNewMessage: (callback: (message: any) => void) => () => void;
|
||||
onMessageRead: (callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => () => void;
|
||||
onUserTyping: (callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => () => void;
|
||||
onMessageRead: (
|
||||
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> = ({
|
||||
children,
|
||||
isAuthenticated = false
|
||||
isAuthenticated = false,
|
||||
}) => {
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
@@ -46,27 +65,20 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
||||
* Initialize socket connection when user is authenticated
|
||||
*/
|
||||
useEffect(() => {
|
||||
console.log('[SocketProvider] useEffect running', { isAuthenticated });
|
||||
|
||||
if (!isAuthenticated) {
|
||||
console.log('[SocketProvider] Not authenticated, skipping socket setup');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[SocketProvider] Initializing socket connection');
|
||||
const newSocket = socketService.connect();
|
||||
setSocket(newSocket);
|
||||
|
||||
// Listen for connection status changes
|
||||
console.log('[SocketProvider] Setting up connection listener');
|
||||
const removeListener = socketService.addConnectionListener((connected) => {
|
||||
console.log('[SocketProvider] Connection status changed:', connected);
|
||||
setIsConnected(connected);
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
console.log('[SocketProvider] Cleaning up connection listener');
|
||||
removeListener();
|
||||
};
|
||||
}, [isAuthenticated]);
|
||||
@@ -76,7 +88,6 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated && socket) {
|
||||
console.log('[SocketProvider] User logged out, disconnecting socket');
|
||||
socketService.disconnect();
|
||||
setSocket(null);
|
||||
setIsConnected(false);
|
||||
@@ -114,9 +125,12 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
||||
/**
|
||||
* Emit mark message as read event
|
||||
*/
|
||||
const emitMarkMessageRead = useCallback((messageId: string, senderId: string) => {
|
||||
const emitMarkMessageRead = useCallback(
|
||||
(messageId: string, senderId: string) => {
|
||||
socketService.emitMarkMessageRead(messageId, senderId);
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Listen for new messages
|
||||
@@ -128,16 +142,34 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
||||
/**
|
||||
* Listen for message read events
|
||||
*/
|
||||
const onMessageRead = useCallback((callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => {
|
||||
const onMessageRead = useCallback(
|
||||
(
|
||||
callback: (data: {
|
||||
messageId: string;
|
||||
readAt: string;
|
||||
readBy: string;
|
||||
}) => void
|
||||
) => {
|
||||
return socketService.onMessageRead(callback);
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Listen for typing indicators
|
||||
*/
|
||||
const onUserTyping = useCallback((callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => {
|
||||
const onUserTyping = useCallback(
|
||||
(
|
||||
callback: (data: {
|
||||
userId: string;
|
||||
firstName: string;
|
||||
isTyping: boolean;
|
||||
}) => void
|
||||
) => {
|
||||
return socketService.onUserTyping(callback);
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const value: SocketContextType = {
|
||||
socket,
|
||||
@@ -153,9 +185,7 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={value}>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -167,7 +197,7 @@ export const useSocket = (): SocketContextType => {
|
||||
const context = useContext(SocketContext);
|
||||
|
||||
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;
|
||||
|
||||
@@ -208,19 +208,11 @@ const ItemDetail: React.FC = () => {
|
||||
const dayTimes = item.weeklyTimes[dayName];
|
||||
availableAfter = dayTimes.availableAfter;
|
||||
availableBefore = dayTimes.availableBefore;
|
||||
console.log("Using day-specific times:", {
|
||||
availableAfter,
|
||||
availableBefore,
|
||||
});
|
||||
}
|
||||
// Otherwise use global times
|
||||
else if (item.availableAfter && item.availableBefore) {
|
||||
availableAfter = item.availableAfter;
|
||||
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 (options.length === 0) {
|
||||
console.log("No valid time options found, showing Not Available");
|
||||
options.push({ value: "00:00", label: "Not Available" });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Conversation, Message, User } from '../types';
|
||||
import { messageAPI } from '../services/api';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useSocket } from '../contexts/SocketContext';
|
||||
import ChatWindow from '../components/ChatWindow';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Conversation, Message, User } from "../types";
|
||||
import { messageAPI } from "../services/api";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { useSocket } from "../contexts/SocketContext";
|
||||
import ChatWindow from "../components/ChatWindow";
|
||||
|
||||
const Messages: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
@@ -23,17 +23,16 @@ const Messages: React.FC = () => {
|
||||
if (!isConnected) return;
|
||||
|
||||
const cleanup = onNewMessage((newMessage: Message) => {
|
||||
console.log('[Messages] Received new message:', newMessage);
|
||||
|
||||
setConversations((prevConversations) => {
|
||||
// Determine conversation partner
|
||||
const partnerId = newMessage.senderId === user?.id
|
||||
const partnerId =
|
||||
newMessage.senderId === user?.id
|
||||
? newMessage.receiverId
|
||||
: newMessage.senderId;
|
||||
|
||||
// Find existing conversation
|
||||
const existingIndex = prevConversations.findIndex(
|
||||
c => c.partnerId === partnerId
|
||||
(c) => c.partnerId === partnerId
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
@@ -46,7 +45,7 @@ const Messages: React.FC = () => {
|
||||
content: newMessage.content,
|
||||
senderId: newMessage.senderId,
|
||||
createdAt: newMessage.createdAt,
|
||||
isRead: newMessage.isRead
|
||||
isRead: newMessage.isRead,
|
||||
};
|
||||
conv.lastMessageAt = newMessage.createdAt;
|
||||
|
||||
@@ -58,20 +57,21 @@ const Messages: React.FC = () => {
|
||||
updated[existingIndex] = conv;
|
||||
|
||||
// Re-sort by most recent
|
||||
updated.sort((a, b) =>
|
||||
new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime()
|
||||
updated.sort(
|
||||
(a, b) =>
|
||||
new Date(b.lastMessageAt).getTime() -
|
||||
new Date(a.lastMessageAt).getTime()
|
||||
);
|
||||
|
||||
console.log('[Messages] Updated existing conversation');
|
||||
return updated;
|
||||
} else {
|
||||
// New conversation - add to top
|
||||
const partner = newMessage.senderId === user?.id
|
||||
const partner =
|
||||
newMessage.senderId === user?.id
|
||||
? newMessage.receiver!
|
||||
: newMessage.sender!;
|
||||
|
||||
if (!partner) {
|
||||
console.warn('[Messages] Partner data missing from new message');
|
||||
return prevConversations;
|
||||
}
|
||||
|
||||
@@ -83,13 +83,13 @@ const Messages: React.FC = () => {
|
||||
content: newMessage.content,
|
||||
senderId: newMessage.senderId,
|
||||
createdAt: newMessage.createdAt,
|
||||
isRead: newMessage.isRead
|
||||
isRead: newMessage.isRead,
|
||||
},
|
||||
unreadCount: newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
|
||||
lastMessageAt: newMessage.createdAt
|
||||
unreadCount:
|
||||
newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
|
||||
lastMessageAt: newMessage.createdAt,
|
||||
};
|
||||
|
||||
console.log('[Messages] Created new conversation');
|
||||
return [newConv, ...prevConversations];
|
||||
}
|
||||
});
|
||||
@@ -103,16 +103,17 @@ const Messages: React.FC = () => {
|
||||
if (!isConnected) return;
|
||||
|
||||
const cleanup = onMessageRead((data: any) => {
|
||||
console.log('[Messages] Message read:', data);
|
||||
|
||||
setConversations((prevConversations) => {
|
||||
return prevConversations.map(conv => {
|
||||
return prevConversations.map((conv) => {
|
||||
// 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 {
|
||||
...conv,
|
||||
lastMessage: { ...conv.lastMessage, isRead: true },
|
||||
unreadCount: Math.max(0, conv.unreadCount - 1)
|
||||
unreadCount: Math.max(0, conv.unreadCount - 1),
|
||||
};
|
||||
}
|
||||
return conv;
|
||||
@@ -127,10 +128,8 @@ const Messages: React.FC = () => {
|
||||
try {
|
||||
const response = await messageAPI.getConversations();
|
||||
setConversations(response.data);
|
||||
console.log('[Messages] Fetched conversations:', response.data.length);
|
||||
} 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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -142,11 +141,17 @@ const Messages: React.FC = () => {
|
||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
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) {
|
||||
return 'Yesterday';
|
||||
return "Yesterday";
|
||||
} 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) => {
|
||||
console.log(`[Messages] ${count} messages marked as read for partner ${partnerId}`);
|
||||
|
||||
// Update the conversation's unread count
|
||||
setConversations(prevConversations =>
|
||||
prevConversations.map(conv =>
|
||||
conv.partnerId === partnerId
|
||||
? { ...conv, unreadCount: 0 }
|
||||
: conv
|
||||
setConversations((prevConversations) =>
|
||||
prevConversations.map((conv) =>
|
||||
conv.partnerId === partnerId ? { ...conv, unreadCount: 0 } : conv
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -201,23 +202,29 @@ const Messages: React.FC = () => {
|
||||
|
||||
{conversations.length === 0 ? (
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="list-group">
|
||||
{conversations.map((conversation) => {
|
||||
const isUnread = conversation.unreadCount > 0;
|
||||
const isLastMessageFromPartner = conversation.lastMessage.senderId === conversation.partnerId;
|
||||
const isLastMessageFromPartner =
|
||||
conversation.lastMessage.senderId === conversation.partnerId;
|
||||
|
||||
return (
|
||||
<div
|
||||
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)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isUnread ? '#f0f7ff' : 'white'
|
||||
cursor: "pointer",
|
||||
backgroundColor: isUnread ? "#f0f7ff" : "white",
|
||||
}}
|
||||
>
|
||||
<div className="d-flex w-100 justify-content-between align-items-start">
|
||||
@@ -228,12 +235,16 @@ const Messages: React.FC = () => {
|
||||
src={conversation.partner.profileImage}
|
||||
alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`}
|
||||
className="rounded-circle me-3"
|
||||
style={{ width: '50px', height: '50px', objectFit: 'cover' }}
|
||||
style={{
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
@@ -242,18 +253,25 @@ const Messages: React.FC = () => {
|
||||
<div className="flex-grow-1" style={{ minWidth: 0 }}>
|
||||
{/* User Name and Unread Badge */}
|
||||
<div className="d-flex align-items-center mb-1">
|
||||
<h6 className={`mb-0 ${isUnread ? 'fw-bold' : ''}`}>
|
||||
{conversation.partner.firstName} {conversation.partner.lastName}
|
||||
<h6 className={`mb-0 ${isUnread ? "fw-bold" : ""}`}>
|
||||
{conversation.partner.firstName}{" "}
|
||||
{conversation.partner.lastName}
|
||||
</h6>
|
||||
{isUnread && (
|
||||
<span className="badge bg-primary ms-2">{conversation.unreadCount}</span>
|
||||
<span className="badge bg-primary ms-2">
|
||||
{conversation.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last Message Preview */}
|
||||
<p
|
||||
className={`mb-0 text-truncate ${isUnread && isLastMessageFromPartner ? 'fw-semibold' : 'text-muted'}`}
|
||||
style={{ maxWidth: '100%' }}
|
||||
className={`mb-0 text-truncate ${
|
||||
isUnread && isLastMessageFromPartner
|
||||
? "fw-semibold"
|
||||
: "text-muted"
|
||||
}`}
|
||||
style={{ maxWidth: "100%" }}
|
||||
>
|
||||
{conversation.lastMessage.senderId === user?.id && (
|
||||
<span className="me-1">You: </span>
|
||||
@@ -264,7 +282,10 @@ const Messages: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{formatDate(conversation.lastMessageAt)}
|
||||
</small>
|
||||
|
||||
@@ -39,7 +39,7 @@ const api = axios.create({
|
||||
export const fetchCSRFToken = async (): Promise<string> => {
|
||||
try {
|
||||
const response = await api.get("/auth/csrf-token");
|
||||
csrfToken = response.data.csrfToken || "";
|
||||
csrfToken = response.headers["x-csrf-token"] || "";
|
||||
return csrfToken || "";
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch CSRF token:", error);
|
||||
@@ -286,10 +286,14 @@ export const forumAPI = {
|
||||
deleteComment: (commentId: string) =>
|
||||
api.delete(`/forum/comments/${commentId}`),
|
||||
// Admin endpoints
|
||||
adminDeletePost: (id: string, reason: string) => api.delete(`/forum/admin/posts/${id}`, { data: { reason } }),
|
||||
adminRestorePost: (id: string) => 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`),
|
||||
adminDeletePost: (id: string, reason: string) =>
|
||||
api.delete(`/forum/admin/posts/${id}`, { data: { reason } }),
|
||||
adminRestorePost: (id: string) =>
|
||||
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`),
|
||||
adminReopenPost: (id: string) => api.patch(`/forum/admin/posts/${id}/reopen`),
|
||||
};
|
||||
|
||||
@@ -52,12 +52,9 @@ class SocketService {
|
||||
*/
|
||||
connect(): Socket {
|
||||
if (this.socket?.connected) {
|
||||
console.log("[Socket] Already connected");
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
console.log("[Socket] Connecting to server...");
|
||||
|
||||
this.socket = io(this.getSocketUrl(), {
|
||||
withCredentials: true, // Send cookies for authentication
|
||||
transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling
|
||||
@@ -71,23 +68,15 @@ class SocketService {
|
||||
|
||||
// Connection event handlers
|
||||
this.socket.on("connect", () => {
|
||||
console.log("[Socket] Connected successfully", {
|
||||
socketId: this.socket?.id,
|
||||
});
|
||||
this.reconnectAttempts = 0;
|
||||
this.notifyConnectionListeners(true);
|
||||
});
|
||||
|
||||
this.socket.on("disconnect", (reason) => {
|
||||
console.log("[Socket] Disconnected", { reason });
|
||||
this.notifyConnectionListeners(false);
|
||||
});
|
||||
|
||||
this.socket.on("connect_error", (error) => {
|
||||
console.error("[Socket] Connection error", {
|
||||
error: error.message,
|
||||
attempt: this.reconnectAttempts + 1,
|
||||
});
|
||||
this.reconnectAttempts++;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -108,7 +93,6 @@ class SocketService {
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.socket) {
|
||||
console.log("[Socket] Disconnecting...");
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
this.notifyConnectionListeners(false);
|
||||
@@ -138,7 +122,6 @@ class SocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Socket] Joining conversation", { otherUserId });
|
||||
this.socket.emit("join_conversation", { otherUserId });
|
||||
}
|
||||
|
||||
@@ -150,7 +133,6 @@ class SocketService {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Socket] Leaving conversation", { otherUserId });
|
||||
this.socket.emit("leave_conversation", { otherUserId });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user