diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..e91fc8c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,46 @@ +name: "CodeQL" + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "*" ] + schedule: + - cron: '0 0 * * 0' # Weekly scan + +jobs: + analyze: + name: CodeQL Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] # Scan JavaScript only + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' # or your Node version + + - name: Install dependencies + run: npm install + + - name: Build project + run: npm run build --if-present + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/README.md b/README.md index 1492d6d..89e449a 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,27 @@ -# SSD Project — Securing the App - -## Team -- Nuwani Fonseka — OAuth (Google) + DoS (ReDoS) fix -- Hasindu — SQL Injection, Dependency updates -- Sadeesha — CSRF, Hardcoded URL removal -- Tiny — Sensitive Data Exposure, (Rate Limiting moved to Tiny) - -## Original project -- Source (pre-fixes): - *Note:* Original commit history predates semester start. (See assignment brief.) :contentReference[oaicite:0]{index=0} - -## Secured project -- Fork / modified code (with fixes): -- Key branches: - - `main` — stable, after fixes - - `nuwani-Oauth` — OAuth + DoS work - -## My vulnerabilities (Nuwani) -1) **DoS (ReDoS) risk** in tour search - - **Before:** `new RegExp(req.query.city, "i")` built from user input - - **After:** Removed dynamic regex. Added input validation and **MongoDB collation** (`strength: 2`) for case-insensitive match. - - **Endpoints:** - - `GET /api/v1/tours/search?city=Colombo&distance=10&maxGroupSize=5` - - `GET /api/v1/tours/featured` - - `GET /api/v1/tours/count` - - **Files touched:** - - `mainapp/backend/controllers/tourController.js` - - `mainapp/backend/models/Tour.js` (index: `tourSchema.index({ city: 1 }, { collation: { locale: "en", strength: 2 } })`) - - `mainapp/backend/routes/tour.js` (route order hardened) - -2) **OAuth 2.0 (Google) login** - - Added OAuth flow with popup-aware route handlers; state parameter preserved; tokens handled server-side. - - **Files:** `eventapp/backend/server.js`, `frontend/src/pages/Login.js`, `frontend/src/pages/SsoHandler.js`, etc. - -## How to run -```bash -cd mainapp/backend -cp .env.example .env -# .env requires: -# PORT=5003 -# MONGO_URI=mongodb+srv://<...> - -npm install -npm start +# Blissify--Wedding-Management-System + +SLIIT 2nd year second semester ITP module project using MERN stack technology for a wedding management system. + +

+Blissify +

+ +

Demo Video: https://youtu.be/ZZwhkC4C63M

+ +

Project Architecture

+ +Micro frontend architecture was implemented to reduce complexity and scale the system according to the client's requirements. With this architecture system breaks down to subdomains. In development, these subdomains run on different ports which was implemented using lerna and craco npm libraries. mainapp works as host app and other apps such as vendorapp, guestapp work as remote apps. + +

+banner

+ +Contributors + +1. Sadeesha : Perera RLSB +1. Ashika : Ridimahaliyadda AK +1. Chamika : Wijenarayana CH +1. Janudi : Adhikari AAJT +1. Dilsha : De Silva KDL +1. Sunera : Weerakkody WSS +1. Amandi : Wickramarathna ACS +1. Nethum : Rathninda RGSNC diff --git a/feedbackapp/backend/.gitignore b/feedbackapp/backend/.gitignore index 30bc162..7af7f04 100644 --- a/feedbackapp/backend/.gitignore +++ b/feedbackapp/backend/.gitignore @@ -1 +1,2 @@ -/node_modules \ No newline at end of file +/node_modules +.env \ No newline at end of file diff --git a/feedbackapp/backend/server.js b/feedbackapp/backend/server.js index f23fe7c..3193ca6 100644 --- a/feedbackapp/backend/server.js +++ b/feedbackapp/backend/server.js @@ -1,33 +1,33 @@ -const express = require('express'); +const express = require("express"); const app = express(); -const cors = require('cors'); +const cors = require("cors"); const port = 3001; -const host = 'localhost'; -const mongoose = require('mongoose'); -const router = require('./routes/feedbackRouter'); +const host = "localhost"; +const mongoose = require("mongoose"); +const router = require("./routes/feedbackRouter"); app.use(cors()); //cors origin unblocking(cross origine resoures sharing) app.use(express.json()); -const uri = 'mongodb+srv://sunera:feedback1211@cluster0.j2j5esj.mongodb.net/feedback_db?retryWrites=true&w=majority&appName=Cluster0'; +// Use environment variable for MongoDB URI +const uri = process.env.MONGO_URI; + const connect = async () => { - try { - await mongoose.connect(uri); - console.log('connected to mongoDB'); - } catch (error) { - console.log('mongoDB error: ',error); - - } + try { + await mongoose.connect(uri); + console.log("connected to mongoDB"); + } catch (error) { + console.log("mongoDB error: ", error); + } }; connect(); //call back function -const server = app.listen(port,host, () => { - console.log(`Node server is listing to ${server.address().port}`) //check actually working sever? - +const server = app.listen(port, host, () => { + console.log(`Node server is listing to ${server.address().port}`); //check actually working sever? }); -app.use('/api',router); \ No newline at end of file +app.use("/api", router); diff --git a/mainapp/backend/routes/auth.js b/mainapp/backend/routes/auth.js index cdf6b31..e2ad48d 100644 --- a/mainapp/backend/routes/auth.js +++ b/mainapp/backend/routes/auth.js @@ -1,9 +1,18 @@ -import express from 'express'; - -import { register,login } from './../controlles/authController.js' +import express from "express"; +import rateLimit from "express-rate-limit"; +import { register, login } from "./../controlles/authController.js"; const router = express.Router(); -router.post('/register',register); -router.post('/login',login); -export default router; \ No newline at end of file +// Rate limiter for login/register endpoints +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // only 5 attempts per IP per window + message: { error: "Too many attempts, please try again later." } +}); + +// Apply limiter only to sensitive routes +router.post("/register", authLimiter, register); +router.post("/login", authLimiter, login); + +export default router; diff --git a/mainapp/backend/routes/booking.js b/mainapp/backend/routes/booking.js index d736722..44e44dc 100644 --- a/mainapp/backend/routes/booking.js +++ b/mainapp/backend/routes/booking.js @@ -1,11 +1,37 @@ -import express from 'express' -import { createBooking, getAllBooking, getBooking } from './../controlles/bookingController.js'; -import { verifyAdmin, verifyUser } from '../utils/verifyToken.js'; -import { get } from 'mongoose'; -const router = express.Router();//const router=express.Router() -router.post('/',createBooking); -router.get('/',verifyUser,getBooking); -router.get('/',verifyAdmin,getAllBooking); +import express from "express"; +import rateLimit from "express-rate-limit"; +import { + createBooking, + getAllBooking, + getBooking +} from "./../controlles/bookingController.js"; +import { verifyAdmin, verifyUser } from "../utils/verifyToken.js"; +const router = express.Router(); -export default router; \ No newline at end of file +// Booking limiter (strict - protects writes) +const bookingLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // allow max 10 requests per IP + message: { error: "Too many booking attempts, please try again later." } +}); + +// Public GET limiter (for read-only database operations) +const readLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + message: { error: "Too many requests, please try again later." } +}); + +// ===== Routes ===== + +// Create a booking (rate limit first → then verify → then controller) +router.post("/", bookingLimiter, verifyUser, createBooking); + +// Get bookings (user's own bookings) +router.get("/", readLimiter, verifyUser, getBooking); + +// Get all bookings (admin only) +router.get("/all", readLimiter, verifyAdmin, getAllBooking); + +export default router; diff --git a/mainapp/backend/routes/review.js b/mainapp/backend/routes/review.js index 71098d2..dd5e98e 100644 --- a/mainapp/backend/routes/review.js +++ b/mainapp/backend/routes/review.js @@ -1,6 +1,18 @@ -import express from 'express' -import { createReview } from './../controlles/reviewController.js'; -import { verifyUser } from '../utils/verifyToken.js'; -const router = express.Router();//const router=express.Router() -router.post('/:tourId',verifyUser,createReview) -export default router; \ No newline at end of file +import express from "express"; +import rateLimit from "express-rate-limit"; +import { createReview } from "./../controlles/reviewController.js"; +import { verifyUser } from "../utils/verifyToken.js"; + +const router = express.Router(); + +// Rate limiter for review creation (max 10 requests per 15 minutes per IP) +const reviewLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + message: { error: "Too many review attempts, please try again later." } +}); + +//Rate limiter comes BEFORE verifyUser → fixes CodeQL warning +router.post("/:tourId", reviewLimiter, verifyUser, createReview); + +export default router; diff --git a/mainapp/backend/routes/tour.js b/mainapp/backend/routes/tour.js index 77f601f..664164c 100644 --- a/mainapp/backend/routes/tour.js +++ b/mainapp/backend/routes/tour.js @@ -1,5 +1,6 @@ // mainapp/backend/routes/tour.js import express from "express"; +import rateLimit from "express-rate-limit"; import { createTour, updateTour, @@ -8,47 +9,45 @@ import { getAllTour, getTourBysearch, getFeaturedTour, - getTourCount, -} from "./../controlles/tourController.js"; // <-- NOTE: 'controllers', not 'controlles' + getTourCount +} from "./../controllers/tourController.js"; import { verifyAdmin } from "../utils/verifyToken.js"; const router = express.Router(); +// Admin limiter (strict) +const adminLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + message: { error: "Too many admin requests, please try again later." } +}); + +// Public GET limiter (generous) +const readLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + message: { error: "Too many requests, please try again later." } +}); + /** * Public search & info endpoints * Put specific routes BEFORE any param route like "/:id" */ - -// GET /api/v1/tours/search?city=...&distance=...&maxGroupSize=... -router.get("/search", getTourBysearch); - -// GET /api/v1/tours/featured -router.get("/featured", getFeaturedTour); - -// GET /api/v1/tours/count -router.get("/count", getTourCount); +router.get("/search", readLimiter, getTourBysearch); +router.get("/featured", readLimiter, getFeaturedTour); +router.get("/count", readLimiter, getTourCount); /** - * CRUD endpoints (admin-protected where needed) + * CRUD endpoints (admin-protected with stricter rate limiting) */ - -// POST /api/v1/tours -router.post("/", verifyAdmin, createTour); - -// PUT /api/v1/tours/:id -router.put("/:id", verifyAdmin, updateTour); - -// DELETE /api/v1/tours/:id -router.delete("/:id", verifyAdmin, deleteTour); +router.post("/", adminLimiter, verifyAdmin, createTour); +router.put("/:id", adminLimiter, verifyAdmin, updateTour); +router.delete("/:id", adminLimiter, verifyAdmin, deleteTour); /** * Param route AFTER specific routes so it won't swallow /search /featured /count */ - -// GET /api/v1/tours/:id -router.get("/:id", getSingleTour); - -// GET /api/v1/tours -router.get("/", getAllTour); +router.get("/:id", readLimiter, getSingleTour); +router.get("/", readLimiter, getAllTour); export default router; diff --git a/mainapp/backend/routes/user.js b/mainapp/backend/routes/user.js index aff6e60..8f96f9f 100644 --- a/mainapp/backend/routes/user.js +++ b/mainapp/backend/routes/user.js @@ -1,17 +1,37 @@ -import express from 'express' -import { updateUser,deleteUser,getSingleUser,getAllUser } from '../controlles/userController.js' +import express from "express"; +import rateLimit from "express-rate-limit"; +import { + updateUser, + deleteUser, + getSingleUser, + getAllUser +} from "../controlles/userController.js"; +import { verifyAdmin, verifyUser } from "../utils/verifyToken.js"; const router = express.Router(); -import { verifyAdmin, verifyUser } from '../utils/verifyToken.js'; -//update User -router.put('/:id',verifyUser,updateUser); -//delete User -router.delete('/:id',verifyUser,deleteUser); +// General limiter +const userLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 20, + message: { error: "Too many requests, please slow down." } +}); -//get Single User -router.get('/:id',verifyUser,getSingleUser); +// Stricter limiter for sensitive ops +const sensitiveLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + message: { error: "Too many attempts, please try again later." } +}); -//get All User -router.get('/',verifyAdmin,getAllUser); -export default router; \ No newline at end of file +/** + * ✅ Fix: Apply limiter BEFORE verifyUser/verifyAdmin + */ + +// Routes +router.put("/:id", sensitiveLimiter, verifyUser, updateUser); +router.delete("/:id", sensitiveLimiter, verifyUser, deleteUser); +router.get("/:id", userLimiter, verifyUser, getSingleUser); +router.get("/", userLimiter, verifyAdmin, getAllUser); + +export default router; diff --git a/packageapp/backend/index.js b/packageapp/backend/index.js index 8175990..c670c1e 100644 --- a/packageapp/backend/index.js +++ b/packageapp/backend/index.js @@ -1,111 +1,104 @@ -const express = require('express') -const app = express() -const port = process.env.PORT || 5000; -const cors = require('cors') - -//middleware -app.use(cors()); -app.use(express.json()); - +const express = require("express"); +const app = express(); +const cors = require("cors"); +const rateLimit = require("express-rate-limit"); +require("dotenv").config(); // 🔹 Load environment variables +const { MongoClient, ServerApiVersion, ObjectId } = require("mongodb"); -//pw -user123 +const port = process.env.PORT || 5000; -app.get('/', (req, res) => { - res.send('Hello World!') -}) +// ===== Rate Limiting ===== +// General limiter (all routes) +const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 min + max: 100, + message: { error: "Too many requests, please try again later." } +}); -//mongodb configuration +// Stricter limiter for write-heavy routes +const writeLimiter = rateLimit({ + windowMs: 10 * 60 * 1000, // 10 min + max: 20, + message: { error: "Too many write requests, slow down." } +}); +// ===== Middleware ===== +app.use(cors()); +app.use(express.json()); +app.use(apiLimiter); // ✅ Apply global limiter -const { MongoClient, ServerApiVersion, ObjectId } = require('mongodb'); -const uri = "mongodb+srv://demo-user:user123@cluster0.aobbdul.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"; +// ===== MongoDB configuration ===== +const uri = process.env.MONGO_URI; // 🔹 No hardcoded credentials -// Create a MongoClient with a MongoClientOptions object to set the Stable API version const client = new MongoClient(uri, { serverApi: { version: ServerApiVersion.v1, strict: true, - deprecationErrors: true, + deprecationErrors: true } }); async function run() { try { - // Connect the client to the server (optional starting in v4.7) await client.connect(); - //create a collection of documents - const Packagecollection = await client.db("PackageInventory").collection("Packages"); - + const Packagecollection = await client + .db("PackageInventory") + .collection("Packages"); - //insert a package to the database post method - - app.post("/upload-Package",async(req,res) => { - const data = req.body; - const result = await Packagecollection.insertOne(data); - res.send(result); + // Insert a package + app.post("/upload-Package", writeLimiter, async (req, res) => { + const data = req.body; + const result = await Packagecollection.insertOne(data); + res.send(result); }); + // Get all packages + app.get("/all-Packages", async (req, res) => { + const packages = Packagecollection.find(); + const result = await packages.toArray(); + res.send(result); + }); - //get all data from the database - - app.get("/all-Packages",async(reg,res)=>{ - const packages = Packagecollection.find(); - const result = await packages.toArray(); - res.send(result); - }) - - - //update a package data : patch or update methods - - app.patch("/Package/:id",async(req,res) => { - const id = req.params.id; - //console.log(id); - const updatePackageData = req.body; - const filter = {_id: new ObjectId(id)}; - const options = {upsert: true}; - const updateDoc ={ - $set: { - ...updatePackageData - } - } - - //update - const result = await Packagecollection.updateOne(filter,updateDoc,options ); - res.send(result); - }) - - - //delete package data - - app.delete("/Package/:id",async(req,res) => { - const id = req.params.id; - const filter = {_id: new ObjectId(id)}; - const result = await Packagecollection.deleteOne(filter); - res.send(result); - }) + // Update a package + app.patch("/Package/:id", writeLimiter, async (req, res) => { + const id = req.params.id; + const updatePackageData = req.body; + const filter = { _id: new ObjectId(id) }; + const options = { upsert: true }; + const updateDoc = { $set: { ...updatePackageData } }; + const result = await Packagecollection.updateOne( + filter, + updateDoc, + options + ); + res.send(result); + }); - //To get single package data + // Delete package + app.delete("/Package/:id", writeLimiter, async (req, res) => { + const id = req.params.id; + const filter = { _id: new ObjectId(id) }; + const result = await Packagecollection.deleteOne(filter); + res.send(result); + }); - app.get("/package/:id", async(req,res) => { + // Get single package + app.get("/package/:id", async (req, res) => { const id = req.params.id; - const filter = {_id:new ObjectId(id)}; + const filter = { _id: new ObjectId(id) }; const result = await Packagecollection.findOne(filter); res.send(result); - }) - - // Send a ping to confirm a successful connection + }); await client.db("admin").command({ ping: 1 }); - console.log("Pinged your deployment. You successfully connected to MongoDB!"); + console.log("✅ MongoDB connection successful!"); } finally { - // Ensures that the client will close when you finish/error - //await client.close(); + // keep client open } } run().catch(console.dir); - app.listen(port, () => { - console.log(`Example app listening on port ${port}`) -}) \ No newline at end of file + console.log(`Example app listening on port ${port}`); +}); diff --git a/taskapp/backend/Routes/NoteRoutes.js b/taskapp/backend/Routes/NoteRoutes.js index 5bb760f..ee12c8a 100644 --- a/taskapp/backend/Routes/NoteRoutes.js +++ b/taskapp/backend/Routes/NoteRoutes.js @@ -1,55 +1,49 @@ -const noteRoutes = require("express").Router(); +const express = require("express"); +const rateLimit = require("express-rate-limit"); const dataModel = require("../Models/DataModel"); -noteRoutes.get("/getNote", async (req, res) => { +const noteRoutes = express.Router(); + +// Per-route limiter +const noteLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 50, + message: { error: "Too many requests to Note API, please slow down." } +}); + +// GET all Notes +noteRoutes.get("/getNote", noteLimiter, async (req, res) => { const { _id } = req.user; - const newNote = new dataModel({ - _id: _id, - }); let note = await dataModel.findById(_id); - if (!note) note = await newNote.save(); - console.log(note.notes); + if (!note) note = await new dataModel({ _id }).save(); res.json(note.notes); }); -noteRoutes.post("/postNote", async (req, res) => { +// POST a new Note +noteRoutes.post("/postNote", noteLimiter, async (req, res) => { const { _id } = req.user; const note = req.body; - await dataModel - .findByIdAndUpdate({ _id: _id }, { $push: { notes: note } }) - .catch((err) => { - console.log(err); - }); + await dataModel.findByIdAndUpdate({ _id }, { $push: { notes: note } }); res.json({ success: "Posted Successfully" }); }); -noteRoutes.patch("/updateNote/:id", async (req, res) => { +// PATCH update Note +noteRoutes.patch("/updateNote/:id", noteLimiter, async (req, res) => { const { id } = req.params; const { newText } = req.body; - await dataModel - .findOneAndUpdate( - { "notes.id": id }, - { - $set: { - "notes.$.noteText": newText, - }, - }, - { new: true } - ) - .catch((err) => { - console.log(err); - }); + await dataModel.findOneAndUpdate( + { "notes.id": id }, + { $set: { "notes.$.noteText": newText } }, + { new: true } + ); res.json({ success: "Updated successfully" }); }); -noteRoutes.delete("/deleteNote/:id", async (req, res) => { +// DELETE Note +noteRoutes.delete("/deleteNote/:id", noteLimiter, async (req, res) => { const { _id } = req.user; const { id } = req.params; - await dataModel - .findByIdAndUpdate(_id, { $pull: { notes: { id: id } } }) - .catch((err) => { - console.log(err); - }); + await dataModel.findByIdAndUpdate(_id, { $pull: { notes: { id } } }); res.json({ success: "Deleted successfully" }); }); diff --git a/taskapp/backend/Routes/TaskRoutes.js b/taskapp/backend/Routes/TaskRoutes.js index ada367c..72bec83 100644 --- a/taskapp/backend/Routes/TaskRoutes.js +++ b/taskapp/backend/Routes/TaskRoutes.js @@ -1,55 +1,49 @@ -const taskRoutes = require("express").Router(); +const express = require("express"); +const rateLimit = require("express-rate-limit"); const dataModel = require("../Models/DataModel"); -taskRoutes.get("/getTask", async (req, res) => { +const taskRoutes = express.Router(); + +// Per-route limiter +const taskLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 50, + message: { error: "Too many requests to Task API, please slow down." } +}); + +// GET all Tasks +taskRoutes.get("/getTask", taskLimiter, async (req, res) => { const { _id } = req.user; - const newTask = new dataModel({ - _id: _id, - }); let task = await dataModel.findById(_id); - if (!task) task = await newTask.save(); - console.log(task.tasks); + if (!task) task = await new dataModel({ _id }).save(); res.json(task.tasks); }); -taskRoutes.post("/postTask", async (req, res) => { +// POST a new Task +taskRoutes.post("/postTask", taskLimiter, async (req, res) => { const { _id } = req.user; const newTask = req.body; - await dataModel - .findByIdAndUpdate({ _id: _id }, { $push: { tasks: newTask } }) - .catch((err) => { - console.log(err); - }); + await dataModel.findByIdAndUpdate({ _id }, { $push: { tasks: newTask } }); res.json({ success: "Posted Successfully" }); }); -taskRoutes.patch("/updateTask/:id", async (req, res) => { +// PATCH update Task +taskRoutes.patch("/updateTask/:id", taskLimiter, async (req, res) => { const { id } = req.params; const { done } = req.body; - await dataModel - .findOneAndUpdate( - { "tasks.id": id }, - { - $set: { - "tasks.$.done": done, - }, - }, - { new: true } - ) - .catch((err) => { - console.log(err); - }); + await dataModel.findOneAndUpdate( + { "tasks.id": id }, + { $set: { "tasks.$.done": done } }, + { new: true } + ); res.json({ success: "Updated successfully" }); }); -taskRoutes.delete("/deleteTask/:id", async (req, res) => { +// DELETE Task +taskRoutes.delete("/deleteTask/:id", taskLimiter, async (req, res) => { const { _id } = req.user; const { id } = req.params; - await dataModel - .findByIdAndUpdate(_id, { $pull: { tasks: { id: id } } }) - .catch((err) => { - console.log(err); - }); + await dataModel.findByIdAndUpdate(_id, { $pull: { tasks: { id } } }); res.json({ success: "Deleted successfully" }); }); diff --git a/taskapp/backend/Routes/TodoRoutes.js b/taskapp/backend/Routes/TodoRoutes.js index 0623841..f8f4160 100644 --- a/taskapp/backend/Routes/TodoRoutes.js +++ b/taskapp/backend/Routes/TodoRoutes.js @@ -1,57 +1,49 @@ -const todoRoutes = require("express").Router(); +const express = require("express"); +const rateLimit = require("express-rate-limit"); const dataModel = require("../Models/DataModel"); -todoRoutes.get("/getTodo", async (req, res) => { +const todoRoutes = express.Router(); + +// Per-route limiter +const todoLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 50, // 50 requests per IP + message: { error: "Too many requests to Todo API, please slow down." } +}); + +// GET all Todos +todoRoutes.get("/getTodo", todoLimiter, async (req, res) => { const { _id } = req.user; - // console.log("req.user", req.user); - const newTodo = new dataModel({ - _id: _id, - }); let todo = await dataModel.findById(_id); - if (!todo) todo = await newTodo.save(); - console.log(todo.todos); + if (!todo) todo = await new dataModel({ _id }).save(); res.json(todo.todos); }); -todoRoutes.post("/postTodo", async (req, res) => { +// POST a new Todo +todoRoutes.post("/postTodo", todoLimiter, async (req, res) => { const { _id } = req.user; const todo = req.body; - await dataModel - .findByIdAndUpdate({ _id: _id }, { $push: { todos: todo } }) - .catch((err) => { - console.log(err); - }); + await dataModel.findByIdAndUpdate({ _id }, { $push: { todos: todo } }); res.json({ success: "Posted Successfully" }); }); -todoRoutes.patch("/updateTodo/:todoId", async (req, res) => { - // const { _id } = req.user; +// PATCH update Todo +todoRoutes.patch("/updateTodo/:todoId", todoLimiter, async (req, res) => { const { todoId } = req.params; const { status } = req.body; - await dataModel - .findOneAndUpdate( - { "todos.todoId": todoId }, - { - $set: { - "todos.$.status": status, - }, - }, - { new: true } - ) - .catch((err) => { - console.log(err); - }); + await dataModel.findOneAndUpdate( + { "todos.todoId": todoId }, + { $set: { "todos.$.status": status } }, + { new: true } + ); res.json({ success: "Updated successfully" }); }); -todoRoutes.delete("/deleteTodo/:todoId", async (req, res) => { +// DELETE Todo +todoRoutes.delete("/deleteTodo/:todoId", todoLimiter, async (req, res) => { const { _id } = req.user; const { todoId } = req.params; - await dataModel - .findByIdAndUpdate(_id, { $pull: { todos: { todoId: todoId } } }) - .catch((err) => { - console.log(err); - }); + await dataModel.findByIdAndUpdate(_id, { $pull: { todos: { todoId } } }); res.json({ success: "Deleted successfully" }); }); diff --git a/taskapp/backend/index.js b/taskapp/backend/index.js index 689e00e..cb5f9c9 100644 --- a/taskapp/backend/index.js +++ b/taskapp/backend/index.js @@ -12,24 +12,40 @@ const passport = require("passport"); const TodoRoutes = require("./Routes/TodoRoutes"); const NoteRoutes = require("./Routes/NoteRoutes"); const TaskRoutes = require("./Routes/TaskRoutes"); +const rateLimit = require("express-rate-limit"); const PORT = 8080; - const app = express(); + +// 🔹 Auth limiter (sensitive routes) +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, + message: { error: "Too many attempts, please try again later." } +}); + +// 🔹 General API limiter (all /todo, /note, /task) +const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, + message: { error: "Too many requests, please slow down." } +}); + app.use([ cors({ origin: process.env.FRONTEND_DOMAIN, credentials: true, - methods: ["GET", "PUT", "PATCH", "PUT", "DELETE"], + methods: ["GET", "POST", "PUT", "PATCH", "DELETE"] }), express.json(), - express.urlencoded({ extended: true }), + express.urlencoded({ extended: true }) ]); const sessionStore = new MongoStore({ mongoUrl: process.env.MONGO_URL, - collectionName: "session", + collectionName: "session" }); + app.use( session({ secret: process.env.SESSION_SECRET, @@ -37,31 +53,31 @@ app.use( saveUninitialized: true, store: sessionStore, cookie: { - maxAge: 1000 * 60 * 60 * 24, - }, + maxAge: 1000 * 60 * 60 * 24 + } }) ); app.use(passport.initialize()); app.use(passport.session()); +// ================== ROUTES ================== + +// Health check app.get("/", (req, res) => { res.json(" hello "); }); -app.post("/register", async (req, res) => { +// Register +app.post("/register", authLimiter, async (req, res) => { const { userName, email, password } = req.body; const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); - const newAuth = new authModel({ - userName: userName, - email: email, - password: hashedPassword, - }); + const newAuth = new authModel({ userName, email, password: hashedPassword }); try { - const user = await authModel.findOne({ email: email }); - if (user) res.json("Already Registerd"); + const user = await authModel.findOne({ email }); + if (user) res.json("Already Registered"); else { const savedUser = await newAuth.save(); res.send(savedUser); @@ -71,45 +87,42 @@ app.post("/register", async (req, res) => { } }); -//Google authentication using passport - +// Google auth app.get( "/google", passport.authenticate("google", { scope: ["profile", "email"] }) ); - app.get( "/google/callback", passport.authenticate("google", { failureRedirect: process.env.FRONTEND_DOMAIN, - successRedirect: `${process.env.FRONTEND_DOMAIN}/Home`, + successRedirect: `${process.env.FRONTEND_DOMAIN}/Home` }) ); -//For Facebook Authentication - +// Facebook auth app.get("/facebook", passport.authenticate("facebook", { scope: ["email"] })); - app.get( "/facebook/callback", passport.authenticate("facebook", { failureRedirect: process.env.FRONTEND_DOMAIN, - successRedirect: `${process.env.FRONTEND_DOMAIN}/Home`, + successRedirect: `${process.env.FRONTEND_DOMAIN}/Home` }) ); -//Local Login +// Login app.post( "/login", + authLimiter, passport.authenticate("local", { - failureRedirect: process.env.FRONTEND_DOMAIN, + failureRedirect: process.env.FRONTEND_DOMAIN }), (req, res) => { res.json({ success: "successfully logged in" }); } ); -//logout +// Logout app.get("/logout", (req, res, next) => { req.logOut((err) => { if (err) res.send(err); @@ -117,36 +130,35 @@ app.get("/logout", (req, res, next) => { }); }); -app.get("/getUser", (req, res, next) => { - if (req.user) { - res.json(req.user); - } +// Get user +app.get("/getUser", (req, res) => { + if (req.user) res.json(req.user); }); -//Forgot and reset password -app.post("/resetPassword/:id/:token", async (req, res) => { +// Reset Password +app.post("/resetPassword/:id/:token", authLimiter, async (req, res) => { const { id, token } = req.params; - console.log(id); const { newPassword } = req.body; - jwt.verify(token, process.env.JWT_SECRET_KEY, async (err, encode) => { + + jwt.verify(token, process.env.JWT_SECRET_KEY, async (err) => { if (err) return res.send({ Status: "Try again after few minutes" }); - else { - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(newPassword, salt); - authModel - .findByIdAndUpdate({ _id: id }, { password: hashedPassword }) - .then((u) => res.send({ Status: "success" })) - .catch((err) => res.send({ Status: err })); - } + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + authModel + .findByIdAndUpdate({ _id: id }, { password: hashedPassword }) + .then(() => res.send({ Status: "success" })) + .catch((err) => res.send({ Status: err })); }); }); -app.post("/forgotpass", async (req, res) => { +// Forgot Password +app.post("/forgotpass", authLimiter, async (req, res) => { const { email } = req.body; - await authModel.findOne({ email: email }).then((user) => { + await authModel.findOne({ email }).then((user) => { if (!user) return res.send({ Status: "Enter a valid email" }); + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET_KEY, { - expiresIn: "1d", + expiresIn: "1d" }); var transporter = nodemailer.createTransport({ service: "gmail", @@ -155,36 +167,35 @@ app.post("/forgotpass", async (req, res) => { secure: true, auth: { user: "jhonmoorthi85131@gmail.com", - pass: "klxb xvje ygnr qvbo", - }, + pass: "klxb xvje ygnr qvbo" + } }); var mailOptions = { from: "jhonmoorthi85131@gmail.com", to: email, subject: "Forgot password for task manager", - text: `${process.env.FRONTEND_DOMAIN}/ResetPass/${user._id}/${token}`, + text: `${process.env.FRONTEND_DOMAIN}/ResetPass/${user._id}/${token}` }; - transporter.sendMail(mailOptions, function (error, info) { - if (error) { - console.log(error); - } else { - return res.send({ Status: "success" }); - } + transporter.sendMail(mailOptions, function (error) { + if (error) console.log(error); + else return res.send({ Status: "success" }); }); }); }); +// Authenticator middleware const authenticator = (req, res, next) => { - if (!req.isAuthenticated()) { + if (!req.isAuthenticated()) return res.status(401).json({ error: "Login Required" }); - } next(); }; -app.use("/todo", [authenticator, TodoRoutes]); -app.use("/note", [authenticator, NoteRoutes]); -app.use("/task", [authenticator, TaskRoutes]); + +// Apply rate limiter + authenticator to APIs +app.use("/todo", apiLimiter, authenticator, TodoRoutes); +app.use("/note", apiLimiter, authenticator, NoteRoutes); +app.use("/task", apiLimiter, authenticator, TaskRoutes); app.listen(PORT, () => { console.log(`Server Running On Port : ${PORT} `); diff --git a/taskapp/backend/package-lock.json b/taskapp/backend/package-lock.json index 6c4bb3d..f4d9c48 100644 --- a/taskapp/backend/package-lock.json +++ b/taskapp/backend/package-lock.json @@ -16,6 +16,7 @@ "cookie-session": "^2.0.0", "cors": "^2.8.5", "express": "^4.18.2", + "express-rate-limit": "^8.1.0", "express-session": "^1.17.3", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", @@ -34,6 +35,380 @@ "node": "18.x" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -584,6 +959,44 @@ "node": ">= 0.8" } }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -638,6 +1051,23 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", + "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express-session": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", @@ -1020,6 +1450,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1425,6 +1863,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1703,6 +2160,13 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "peer": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1714,6 +2178,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1821,6 +2314,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "peer": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1967,6 +2477,16 @@ "node": ">=10" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -2158,6 +2678,62 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "peer": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/vite-plugin-rewrite-all": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vite-plugin-rewrite-all/-/vite-plugin-rewrite-all-1.0.1.tgz", @@ -2213,6 +2789,182 @@ } }, "dependencies": { + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": true, + "peer": true + }, "@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -2642,6 +3394,37 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "peer": true, + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2735,6 +3518,14 @@ } } }, + "express-rate-limit": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", + "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", + "requires": { + "ip-address": "10.0.1" + } + }, "express-session": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", @@ -2991,6 +3782,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3273,6 +4069,13 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "peer": true + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3475,11 +4278,30 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "peer": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, + "postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "peer": true, + "requires": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3554,6 +4376,16 @@ "glob": "^7.1.3" } }, + "rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "peer": true, + "requires": { + "fsevents": "~2.3.2" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3669,6 +4501,13 @@ "semver": "^7.5.3" } }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "peer": true + }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -3815,6 +4654,19 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "peer": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + }, "vite-plugin-rewrite-all": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vite-plugin-rewrite-all/-/vite-plugin-rewrite-all-1.0.1.tgz", diff --git a/taskapp/backend/package.json b/taskapp/backend/package.json index 105de5f..093d99c 100644 --- a/taskapp/backend/package.json +++ b/taskapp/backend/package.json @@ -22,6 +22,7 @@ "cookie-session": "^2.0.0", "cors": "^2.8.5", "express": "^4.18.2", + "express-rate-limit": "^8.1.0", "express-session": "^1.17.3", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", diff --git a/vendorapp/backend/routes/route.js b/vendorapp/backend/routes/route.js index 17dd10e..7b6ccb9 100644 --- a/vendorapp/backend/routes/route.js +++ b/vendorapp/backend/routes/route.js @@ -1,22 +1,14 @@ const router = require("express").Router(); const authMiddleware = require("../middleware/authMiddleware.js"); -//update const rateLimit = require("express-rate-limit"); -// Rate limiter for login route -const loginLimiter = rateLimit({ - windowMs: 5 * 60 * 1000, // 5 minutes - max: 5, // limit each IP to 5 login attempts per window - message: { error: "Too many login attempts. Please try again later." }, - standardHeaders: true, // Return rate limit info in headers - legacyHeaders: false // Disable the X-RateLimit headers -}); - +// Vendor Controller const { vendorRegister, vendorLogIn } = require("../controllers/vendorController.js"); +// Service Controller const { serviceCreate, getServices, @@ -35,6 +27,7 @@ const { getAddedToInvoiceServices } = require("../controllers/serviceController.js"); +// Couple Controller const { coupleRegister, coupleLogIn, @@ -42,46 +35,102 @@ const { invoiceUpdate } = require("../controllers/coupleController.js"); +// Booking Controller const { newBooking, getBookingedServicesByCouple, getBookingedServicesByVendor } = require("../controllers/bookingController.js"); +// ====== Rate Limiters ====== + +// Strict limiter for login/register (brute-force protection) +const authLimiter = rateLimit({ + windowMs: 5 * 60 * 1000, // 5 minutes + max: 5, // max 5 attempts + message: { error: "Too many attempts. Please try again later." }, + standardHeaders: true, + legacyHeaders: false +}); + +// Moderate limiter for sensitive write actions (create/update/delete) +const writeLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 20, // max 20 actions per IP + message: { error: "Too many requests. Please try again later." }, + standardHeaders: true, + legacyHeaders: false +}); + +// Lighter limiter for search/list endpoints (anti-scraping) +const searchLimiter = rateLimit({ + windowMs: 1 * 60 * 1000, // 1 minute + max: 60, // 60 requests per minute + message: { error: "Too many search requests. Please slow down." }, + standardHeaders: true, + legacyHeaders: false +}); + +// ====== Routes ====== + // Vendor -router.post("/VendorRegister", vendorRegister); -router.post("/VendorLogin", vendorLogIn); +router.post("/VendorRegister", authLimiter, vendorRegister); +router.post("/VendorLogin", authLimiter, vendorLogIn); // Service -router.post("/ServiceCreate", serviceCreate); -router.get("/getVendorServices/:id", getVendorServices); -router.get("/getServices", getServices); -router.get("/getServiceDetail/:id", getServiceDetail); -router.get("/getInterestedCouples/:id", getInterestedCouples); -router.get("/getAddedToInvoiceServices/:id", getAddedToInvoiceServices); +router.post("/ServiceCreate", writeLimiter, serviceCreate); +router.get("/getVendorServices/:id", searchLimiter, getVendorServices); +router.get("/getServices", searchLimiter, getServices); +router.get("/getServiceDetail/:id", searchLimiter, getServiceDetail); +router.get("/getInterestedCouples/:id", searchLimiter, getInterestedCouples); +router.get( + "/getAddedToInvoiceServices/:id", + searchLimiter, + getAddedToInvoiceServices +); -router.put("/ServiceUpdate/:id", updateService); -router.put("/addReview/:id", addReview); +router.put("/ServiceUpdate/:id", writeLimiter, updateService); +router.put("/addReview/:id", writeLimiter, addReview); -router.get("/searchService/:key", searchService); -router.get("/searchServicebyCategory/:key", searchServicebyCategory); -router.get("/searchServicebySubCategory/:key", searchServicebySubCategory); +router.get("/searchService/:key", searchLimiter, searchService); +router.get( + "/searchServicebyCategory/:key", + searchLimiter, + searchServicebyCategory +); +router.get( + "/searchServicebySubCategory/:key", + searchLimiter, + searchServicebySubCategory +); -router.delete("/DeleteService/:id", deleteService); -router.delete("/DeleteServices/:id", deleteServices); -router.put("/deleteServiceReview/:id", deleteServiceReview); -router.delete("/deleteAllServiceReviews/:id", deleteAllServiceReviews); +router.delete("/DeleteService/:id", writeLimiter, deleteService); +router.delete("/DeleteServices/:id", writeLimiter, deleteServices); +router.put("/deleteServiceReview/:id", writeLimiter, deleteServiceReview); +router.delete( + "/deleteAllServiceReviews/:id", + writeLimiter, + deleteAllServiceReviews +); // Couple -router.post("/CoupleRegister", coupleRegister); -router.post("/CoupleLogin", loginLimiter, coupleLogIn); +router.post("/CoupleRegister", authLimiter, coupleRegister); +router.post("/CoupleLogin", authLimiter, coupleLogIn); -router.get("/getInvoiceDetail/:id", getInvoiceDetail); -router.put("/CoupleUpdate/:id", invoiceUpdate); +router.get("/getInvoiceDetail/:id", searchLimiter, getInvoiceDetail); +router.put("/CoupleUpdate/:id", writeLimiter, invoiceUpdate); // Booking -router.post("/newBooking", newBooking); -router.get("/getBookingedServicesByCouple/:id", getBookingedServicesByCouple); -router.get("/getBookingedServicesByVendor/:id", getBookingedServicesByVendor); +router.post("/newBooking", writeLimiter, newBooking); +router.get( + "/getBookingedServicesByCouple/:id", + searchLimiter, + getBookingedServicesByCouple +); +router.get( + "/getBookingedServicesByVendor/:id", + searchLimiter, + getBookingedServicesByVendor +); module.exports = router;