Ver código fonte

added custom input validation

liontix 10 meses atrás
pai
commit
428afef39a
8 arquivos alterados com 150 adições e 56 exclusões
  1. 2 0
      app.js
  2. 3 1
      package.json
  3. 24 11
      routes/appointments.js
  4. 0 1
      routes/auth.js
  5. 16 12
      routes/members.js
  6. 32 10
      schemas/appointmentSchema.js
  7. 8 10
      schemas/bookingSchema.js
  8. 65 11
      utils.js

+ 2 - 0
app.js

@@ -10,6 +10,7 @@ import {router as authRouter} from "./routes/auth.js";
 import {router as memberRouter} from "./routes/members.js";
 import {router as appointmentsRouter} from "./routes/appointments.js";
 import {fileURLToPath} from "url";
+import cors from "cors";
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
@@ -28,6 +29,7 @@ async function connect() {
 app.set('views', path.join(__dirname, 'views'));
 app.set('view engine', 'pug');
 
+app.use(cors());
 app.use(logger('dev'));
 app.use(express.json());
 app.use(express.urlencoded({ extended: false }));

+ 3 - 1
package.json

@@ -10,6 +10,7 @@
   "dependencies": {
     "bcrypt": "^5.1.1",
     "cookie-parser": "~1.4.4",
+    "cors": "^2.8.5",
     "debug": "~2.6.9",
     "dotenv": "^16.4.7",
     "eslinter": "^3.1.0",
@@ -24,6 +25,7 @@
     "nodemon": "^3.1.9",
     "passport": "^0.7.0",
     "passport-jwt": "^4.0.1",
-    "pug": "^3.0.3"
+    "pug": "^3.0.3",
+    "validator": "^13.12.0"
   }
 }

+ 24 - 11
routes/appointments.js

@@ -4,7 +4,7 @@
 import express from 'express';
 import Appointment from "../schemas/appointmentSchema.js";
 import Booking from "../schemas/bookingSchema.js";
-import {isDueDatePassed, isValidEmail} from "../utils.js";
+import {isDueDatePassed, isInputValid, isValidEmail} from "../utils.js";
 
 /**
  * Create an Express router for handling appointment-related routes.
@@ -15,33 +15,46 @@ const router = express.Router();
  * Create a new appointment and associate it with a booking.
  */
 router.post('/create', async function(req, res, next) {
-    const { appointment, email, firstname, lastname, times } = req.body;
+    const { appointmentId, time, inputs } = req.body;
 
-    const appointmentId = appointment;
-
-    if (!appointmentId || !firstname || !lastname || !email || !times) {
+    if (!appointmentId || !time || !inputs) {
+        console.log(appointmentId);
+        console.log(time);
+        console.log(inputs);
         res.status(400).json({ 'message': 'Parameters are incomplete' });
         return;
     }
 
     // TODO validate times
 
-    if (!isValidEmail(email)) {
-        res.status(400).json({ 'message': 'Email is invalid' });
-        return;
-    }
+    // advanced input validation
+    // get constraints from database
+
+
 
     try {
         const existingAppointment = await Appointment.findOne({ _id: appointmentId });
         if (!existingAppointment) {
             return res.status(404).json({ message: 'appointment does not exist' });
         }
+        
+        console.log(existingAppointment.times);
+        // existingAppointment.times.find(input => input.date === "2025-01-27T06:00:00.000Z")
+        if (existingAppointment.times.find(input => input.date === "2025-01-27T06:00:00.000Z")) {
+            console.log("done");
+        }
+
+
+        if (!isInputValid(inputs, existingAppointment.inputs)) {
+            res.status(400).json({ 'message': 'Custom inputs are invalid' });
+            return;
+        }
 
         if (isDueDatePassed(existingAppointment["dueDate"])) {
             return res.status(401).json({ message: 'appointment registration is closed' });
         }
 
-        const duplicateBooking = await Booking.findOne({ appointment: appointmentId, email: email })
+        const duplicateBooking = await Booking.findOne({ appointment: appointmentId, time, inputs: inputs })
             .catch(err => {
                 console.log(err);
             });
@@ -50,7 +63,7 @@ router.post('/create', async function(req, res, next) {
             return res.status(404).json({ message: 'already registered' });
         }
 
-        const newBooking = new Booking({ appointment, email, firstname, lastname });
+        const newBooking = new Booking({ appointment: appointmentId, time, inputs });
 
         newBooking.save()
             .then(doc => res.json({ "success": true }))

+ 0 - 1
routes/auth.js

@@ -87,7 +87,6 @@ router.post('/login', function (req, res) {
             if (!user) {
                 return res.status(422).json({ message: 'user not found' });
             }
-            console.log(user);
             bcrypt.compare(password, user.password, function (err, result) {
                 if (result) {
                     res.json({ token: generateToken(user) });

+ 16 - 12
routes/members.js

@@ -3,7 +3,7 @@ import passport from "passport";
 import {mongoose} from "mongoose";
 import Appointment from "../schemas/appointmentSchema.js";
 import Booking from "../schemas/bookingSchema.js";
-import {appointmentTimeSchema, isDueDatePassed, isValidAppointmentTimes, validateDates} from "../utils.js";
+import {appointmentTimeSchema, isDueDatePassed, isValidAppointmentTimes, isValidCustomInput, validateDates} from "../utils.js";
 
 /**
  * Router for handling appointments.
@@ -30,27 +30,30 @@ router.use((req, res, next) => {
  */
 router.post('/create', function(req, res, next) {
     const user = req.userId;
-    const { title, description, dueDate, place, duration, times } = req.body;
+    const { title, description, dueDate, place, duration, times, inputs } = req.body;
 
-    if (!title || title.length === 0 || !dueDate || !description || !duration || !place || !times) {
+    if (!title || title.length === 0 || !dueDate || !description || !duration || !place || !times || !inputs) {
         res.status(400).json({ 'message': 'Empty parameter' });
         return;
     }
     ///
-    if (!isDueDatePassed(dueDate)) {
-        res.status(400).json({ 'message': 'Due date is in the future' });
+    if (isDueDatePassed(dueDate)) {
+        res.status(400).json({ 'message': 'Due date is invalid or in the past' });
         return;
     }
 
-    console.log(times);
-
     // validate times
     if (!isValidAppointmentTimes(times)) {
         res.status(400).json({ 'message': 'Appointment times are invalid' });
         return;
     }
 
-    const newTask = new Appointment({ user, title, description, dueDate, place, duration, times });
+    if (!isValidCustomInput(inputs)) {
+        res.status(400).json({ 'message': 'Custom inputs are invalid' });
+        return;
+    }
+
+    const newTask = new Appointment({ user, title, description, dueDate, place, duration, times, inputs });
 
     newTask.save()
         .then(doc => res.json({ "id": doc._id }))
@@ -63,19 +66,20 @@ router.post('/create', function(req, res, next) {
 router.post('/modify', function (req, res, next) {
     const user = req.userId;
 
-    const { appointmentId, title, description, dueDate, place, startDate, endDate } = req.body;
-    if (!title || !dueDate || !description || !startDate || !endDate || !place || !appointmentId) {
+    const { appointmentId, title, description, dueDate, place, times, inputs } = req.body;
+    if (!title || !dueDate || !description || !startDate || !endDate || !times || !inputs) {
         res.status(400).json({ 'error': 'Empty parameter' });
         return;
     }
 
-    if (!validateDates(res, startDate, endDate, dueDate)) {
+    if (isDueDatePassed(dueDate)) {
+        res.status(400).json({ 'error': 'Invalid due date' });
         return;
     }
 
     Appointment.updateOne(
         { _id: appointmentId, user: user },
-        { $set: { title, description, dueDate, place, startDate, endDate } }
+        { $set: { title, description, dueDate, place, times, inputs } }
     )
         .then(result => {
             if (result.modifiedCount === 1) {

+ 32 - 10
schemas/appointmentSchema.js

@@ -1,5 +1,31 @@
 import mongoose from "mongoose";
 
+
+const customInputSchema = new mongoose.Schema({
+    name: {
+        type: String,
+        required: true
+    },
+    type: {
+        type: String,
+        enum: ["tel", "text", "email"],
+        requried: true
+    }
+}, 
+{_id: false});
+
+const customTimesSchema = new mongoose.Schema({
+    date: {
+        type: Date,
+        required: true
+    },
+    available: {
+        type: Boolean,
+        default: false
+    }
+},
+{_id: false});
+
 /**
  * Define a schema for an appointment using mongoose.
  *
@@ -10,8 +36,6 @@ import mongoose from "mongoose";
  * @property {String} description - A longer description of the appointment.
  * @property {Date} dueDate - The date and time when the appointment is scheduled.
  * @property {String} place - The location where the appointment will take place.
- * @property {Date} startDate - The start date and time of the appointment.
- * @property {Date} endDate - The end date and time of the appointment.
  */
 
 /**
@@ -31,27 +55,25 @@ const appointmentSchema = new mongoose.Schema({
         required: true
     },
     description: {
-        type: String,
-        required: true
+        type: String
     },
     dueDate: {
         type: Date,
         required: true
     },
     place: {
-        type: String,
-        required: true
+        type: String
     },
     duration: {
         type: Number,
         required: true
     },
-    times: {
-        type: Array,
-        required: true
-    }
+    times: [customTimesSchema],
+    inputs: [customInputSchema]
 });
 
+
+
 /**
  * Create a mongoose model for appointments and name it "Appointment".
  *

+ 8 - 10
schemas/bookingSchema.js

@@ -1,22 +1,20 @@
 import mongoose from "mongoose";
 
+const bookingInputSchema = new mongoose.Schema({
+    name: { type: String, required: true },
+    value: { type: mongoose.Schema.Types.Mixed, required: true }, // Can be string, number, etc.
+}, {_id: false});
+
 const bookingSchema = new mongoose.Schema({
     appointment: {
         type: String,
         required: true
     },
-    email: {
-        type: String,
+    time: {
+        type: Date,
         required: true
     },
-    firstname: {
-        type: String,
-        required: true
-    },
-    lastname: {
-        type: String,
-        required: true
-    }
+    inputs: [bookingInputSchema]
 });
 const Booking = mongoose.model('Booking', bookingSchema, 'booking');
 

+ 65 - 11
utils.js

@@ -1,10 +1,16 @@
 import Joi from "joi";
 import moment from "moment";
+import validator from "validator";
 
 const appointmentTimeSchema = Joi.object().keys({
     date: Joi.date().required(),
     available: Joi.boolean().required()
-  });
+});
+
+const customInputSchema = Joi.object().keys({
+    name: Joi.string().required(),
+    type: Joi.string().required()
+}).unknown(true);
 
 function isValidEmail(email) {
     const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -12,7 +18,7 @@ function isValidEmail(email) {
 }
 
 function isDueDatePassed(dueDate) {
-    return moment(dueDate, 'YYYY-MM-DD', true).isAfter();
+    return moment(dueDate, 'YYYY-MM-DD', true).isBefore();
 }
 
 function isDatePassed(dateA, dateB) {
@@ -21,16 +27,64 @@ function isDatePassed(dateA, dateB) {
     return dueDateA < dueDateB;
 }
 
-function isValidAppointmentTimes(data) {
-    return data.every(item => {
-      try {
-        if (!moment(item.date, 'YYYY-MM-DDTHH:mm', true).isValid() || appointmentTimeSchema.validate(item)['error']) {
-            return false;
+
+function isInputValid(data, inputs) {
+    if (!isValidCustomInput(inputs)) {
+        return false;
+    }
+
+    // check for duplicates
+    const seenNames = new Set();
+    for (const element of data) {
+        if (seenNames.has(element.name)) {
+            return false; // Duplicate found
+        }
+        seenNames.add(element.name);
+    }
+
+    // validate each input dynamically based on its type
+    return data.every(element => {
+        const inputDefinition = inputs.find(input => input.name === element.name);
+        if (!inputDefinition) {
+            return false; // If the input is not in the expected schema, fail validation
+        }
+
+        switch (inputDefinition.type) {
+            case "email":
+                return validator.isEmail(element.value);
+
+            case "text":
+                return !validator.isEmpty(element.value.trim()); // Trim to prevent spaces from being valid
+
+            case "tel":
+                return validator.isMobilePhone(element.value, "any", { strictMode: false });
+
+            default:
+                return false; // Reject unknown input types
+        }
+    });
+}
+
+function isValidCustomInput(data) {
+    return !data.some(element => {
+        const { error } = customInputSchema.validate(element);
+        if (error) {
+            console.log("Validation failed:", error.details);
+            return true; // `some()` stops when this is true
         }
-      } catch (e) {
         return false;
-      }
-      return true;
+    });
+}
+
+function isValidAppointmentTimes(data) {
+    return data.every(item => {
+        // Validate date format strictly using Moment.js
+        const isDateValid = moment(item.date, 'YYYY-MM-DDTHH:mm', true).isValid();
+
+        // Validate item against the schema
+        const { error } = appointmentTimeSchema.validate(item);
+
+        return isDateValid && !error;
     });
 }
 
@@ -53,4 +107,4 @@ function validateDates(res, startDate, endDate, dueDate) {
     return true;
 }
 
-export {isValidEmail, isDueDatePassed, isDatePassed, validateDates, isValidAppointmentTimes, appointmentTimeSchema};
+export {isValidEmail, isDueDatePassed, isDatePassed, validateDates, isValidAppointmentTimes, appointmentTimeSchema, isValidCustomInput, isInputValid};