diff --git a/controllers/userReportController.js b/controllers/userReportController.js index ff3ce46..d5ce7e9 100644 --- a/controllers/userReportController.js +++ b/controllers/userReportController.js @@ -6,20 +6,23 @@ import { createSystemLog } from "./adminLogsController.js"; export const createUserReport = async (req, res) => { try { const { title, disaster_category, description, location } = req.body; - const user = req.user; - if (!title || !disaster_category || !description || !location) { + // Parse location if it's a string + let locationData; + try { + locationData = + typeof location === "string" ? JSON.parse(location) : location; + } catch (error) { return res.status(400).json({ - error: - "Missing required fields: title, disaster_category, description, and location are required", + error: "Invalid location data format", }); } + // Validate location structure if ( - !location.address || - !location.address.city || - !location.address.district || - !location.address.province + !locationData?.address?.city || + !locationData?.address?.district || + !locationData?.address?.province ) { return res.status(400).json({ error: @@ -27,65 +30,25 @@ export const createUserReport = async (req, res) => { }); } - const validCategories = [ - "flood", - "fire", - "earthquake", - "landslide", - "cyclone", - ]; - if (!validCategories.includes(disaster_category)) { - return res.status(400).json({ - error: "Invalid disaster category", - }); - } - - if (req.body.images) { - const invalidImages = req.body.images.filter( - (url) => !url.startsWith("http"), - ); - if (invalidImages.length > 0) { - return res.status(400).json({ - error: "All images must be valid URLs starting with 'http'", - }); - } - } + // Get image paths from uploaded files + const images = req.files + ? req.files.map((file) => `/uploads/${file.filename}`) + : []; + // Create report data const reportData = { - ...req.body, - verification_status: "pending", - reporter_type: "anonymous", + title, + disaster_category, + description, + location: locationData, + images, }; - if (user) { - reportData.reporter = user._id; - reportData.reporter_type = "registered"; - if (user.isVerified) { - reportData.verification_status = "verified"; - reportData.verification = { - verified_by: user._id, - verified_at: new Date(), - severity: "medium", - }; - } - } const newReport = await UserReports.create(reportData); - - if (user?.isVerified) { - await createSystemLog( - user._id, - "AUTO_VERIFY_REPORT", - "user_report", - newReport._id, - { - message: `Report auto-verified by verified user ${user.name}`, - }, - ); - } - res.status(201).json(newReport); } catch (error) { + console.error("Create report error:", error); res.status(500).json({ error: error.message }); } }; @@ -349,44 +312,43 @@ export const getVerificationStats = async (req, res) => { const stats = await Promise.all([ // Pending reports count UserReports.countDocuments({ verification_status: "pending" }), - + // Verified today count UserReports.countDocuments({ verification_status: "verified", - "verification.verified_at": { $gte: today } + "verification.verified_at": { $gte: today }, }), - + // Active incidents count Warning.countDocuments({ status: "active" }), - + // Average verification time UserReports.aggregate([ { - $match: { + $match: { verification_status: "verified", - "verification.verification_time": { $exists: true } - } + "verification.verification_time": { $exists: true }, + }, }, { $group: { _id: null, - avgTime: { - $avg: { - $divide: ["$verification.verification_time", 60] // Convert minutes to hours - } - } - } - } - ]) + avgTime: { + $avg: { + $divide: ["$verification.verification_time", 60], // Convert minutes to hours + }, + }, + }, + }, + ]), ]); res.json({ pendingCount: stats[0], - verifiedToday: stats[1], + verifiedToday: stats[1], activeIncidents: stats[2], - avgVerificationTime: Math.round(stats[3][0]?.avgTime || 0) + avgVerificationTime: Math.round(stats[3][0]?.avgTime || 0), }); - } catch (error) { console.error("Verification stats error:", error); res.status(500).json({ error: error.message }); @@ -405,48 +367,50 @@ export const getReportAnalytics = async (req, res) => { weeklyTrends: [ { $match: { - createdAt: { $gte: weekAgo } - } + createdAt: { $gte: weekAgo }, + }, }, { $group: { _id: { - date: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }, - status: "$verification_status" + date: { + $dateToString: { format: "%Y-%m-%d", date: "$createdAt" }, + }, + status: "$verification_status", }, - count: { $sum: 1 } - } + count: { $sum: 1 }, + }, }, { $project: { _id: 0, date: "$_id.date", status: "$_id.status", - count: 1 - } - } + count: 1, + }, + }, ], reportTypes: [ { $group: { _id: "$disaster_category", - count: { $sum: 1 } - } + count: { $sum: 1 }, + }, }, { $project: { name: { $ifNull: ["$_id", "Unknown"] }, value: "$count", - _id: 0 - } - } + _id: 0, + }, + }, ], responseTime: [ { $match: { verification_status: "verified", - "verification.verification_time": { $exists: true } - } + "verification.verification_time": { $exists: true }, + }, }, { $bucket: { @@ -454,9 +418,9 @@ export const getReportAnalytics = async (req, res) => { boundaries: [0, 60, 120, 240, Infinity], default: "other", output: { - count: { $sum: 1 } - } - } + count: { $sum: 1 }, + }, + }, }, { $project: { @@ -466,23 +430,23 @@ export const getReportAnalytics = async (req, res) => { { case: { $eq: ["$_id", 0] }, then: "<1h" }, { case: { $eq: ["$_id", 60] }, then: "1-2h" }, { case: { $eq: ["$_id", 120] }, then: "2-4h" }, - { case: { $eq: ["$_id", 240] }, then: ">4h" } + { case: { $eq: ["$_id", 240] }, then: ">4h" }, ], - default: "other" - } + default: "other", + }, }, count: 1, - _id: 0 - } - } - ] - } - } + _id: 0, + }, + }, + ], + }, + }, ]); // Format weekly trends data const trendsMap = {}; - analytics[0].weeklyTrends.forEach(item => { + analytics[0].weeklyTrends.forEach((item) => { if (!trendsMap[item.date]) { trendsMap[item.date] = { date: item.date }; } @@ -492,7 +456,7 @@ export const getReportAnalytics = async (req, res) => { const formattedResponse = { weeklyTrends: Object.values(trendsMap), reportTypes: analytics[0].reportTypes, - responseTime: analytics[0].responseTime + responseTime: analytics[0].responseTime, }; res.json(formattedResponse); @@ -637,7 +601,7 @@ export const getFeedStats = async (req, res) => { warningStats, activeWarnings: warningStats.reduce( (acc, curr) => acc + curr.active_warnings, - 0 + 0, ), }, }); diff --git a/index.js b/index.js index 579fe72..0578abf 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,18 @@ import { errorHandler, routeNotFound } from "./middlewares/errorMiddleware.js"; import routes from "./routes/index.js"; import Scheduler from "./utils/scheduler.js"; import DevScheduler from "./utils/devScheduler.js"; +import path from "path"; +import { fileURLToPath } from "url"; +import fs from "fs"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Create uploads directory if it doesn't exist +const uploadsDir = path.join(__dirname, "uploads"); +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} dotenv.config(); connectMongoose(); @@ -47,13 +58,16 @@ app.use(express.urlencoded({ extended: true })); app.use(morgan("dev")); +app.use("/api/uploads", express.static("uploads")); app.use("/api", routes); app.use(routeNotFound); app.use(errorHandler); if (process.env.NODE_ENV !== "dev") { - app.listen(PORT, () => console.log(`Server is running on port ${PORT}`)); + app.listen(5000, "0.0.0.0", () => { + console.log("Server running on port 5000"); + }); } export default app; diff --git a/middlewares/upload.js b/middlewares/upload.js new file mode 100644 index 0000000..86d6c37 --- /dev/null +++ b/middlewares/upload.js @@ -0,0 +1,37 @@ +import multer from 'multer'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + const uploadPath = path.join(__dirname, '../uploads'); + cb(null, uploadPath); + }, + filename: function (req, file, cb) { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, uniqueSuffix + path.extname(file.originalname)); + } +}); + +const upload = multer({ + storage: storage, + limits: { + fileSize: 5 * 1024 * 1024 // 5MB limit + }, + fileFilter: function (req, file, cb) { + const allowedTypes = /jpeg|jpg|png/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + + if (extname && mimetype) { + return cb(null, true); + } else { + cb(new Error('Only images are allowed!')); + } + } +}); + +export default upload; \ No newline at end of file diff --git a/models/userReports.js b/models/userReports.js index 1d8955c..cefb3a5 100644 --- a/models/userReports.js +++ b/models/userReports.js @@ -41,15 +41,12 @@ const userReportSchema = new Schema( images: { type: [String], validate: { - validator: function (v) { - return ( - !v.length || - v.every((url) => typeof url === "string" && url.startsWith("http")) - ); + validator: function(v) { + return !v.length || v.every(url => typeof url === 'string'); }, - message: "All images must be valid URLs", + message: 'All images must be valid paths or URLs' }, - required: false, + required: false }, reporter: { type: Schema.Types.ObjectId, diff --git a/package-lock.json b/package-lock.json index 353616f..851c821 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "mongoose": "^8.9.5", "mongoose-gridfs": "^1.3.0", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", "nodemon": "^3.1.7" }, @@ -2639,6 +2640,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -3057,9 +3064,19 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3301,6 +3318,21 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -3378,6 +3410,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4531,6 +4569,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5610,6 +5654,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -5744,6 +5800,7 @@ "version": "8.9.5", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz", "integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==", + "license": "MIT", "dependencies": { "bson": "^6.10.1", "kareem": "2.6.3", @@ -5868,6 +5925,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6301,6 +6376,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6449,6 +6530,27 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6947,6 +7049,14 @@ "dezalgo": "^1.0.1" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/streamx": { "version": "2.21.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz", @@ -6962,6 +7072,21 @@ "bare-events": "^2.2.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -7252,6 +7377,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typpy": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/typpy/-/typpy-2.3.11.tgz", @@ -7358,6 +7489,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -7499,6 +7636,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index ff63f37..69fe829 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ "test:watch": "NODE_ENV=test jest --watch" }, "jest": { - "testEnvironment": "node", - "transform": { - "^.+\\.js$": "babel-jest" - } - }, + "testEnvironment": "node", + "transform": { + "^.+\\.js$": "babel-jest" + } + }, "keywords": [ "disasterwatch" ], @@ -33,6 +33,7 @@ "mongoose": "^8.9.5", "mongoose-gridfs": "^1.3.0", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", "nodemon": "^3.1.7" }, diff --git a/routes/userReportRoutes.js b/routes/userReportRoutes.js index f5d7c04..d9c4a7f 100644 --- a/routes/userReportRoutes.js +++ b/routes/userReportRoutes.js @@ -18,11 +18,12 @@ import { verifyVerifiedUser, verifyToken, } from "../middlewares/authMiddleware.js"; +import upload from '../middlewares/upload.js'; const router = express.Router(); router.get("/feed", getPublicFeed); -router.post("/", createUserReport); +router.post("/",upload.array('images', 3), createUserReport); router.get("/public", getUserReports); router.get('/reports', getFeedReports); router.get('/feedstats', getFeedStats);