diff --git a/_data/bootcamps.json b/_data/bootcamps.json new file mode 100644 index 0000000..20f791e --- /dev/null +++ b/_data/bootcamps.json @@ -0,0 +1,67 @@ +[ + { + "_id": "5d713995b721c3bb38c1f5d0", + "user": "5d7a514b5d2c12c7449be045", + "name": "Devworks Bootcamp", + "description": "Devworks is a full stack JavaScript Bootcamp located in the heart of Boston that focuses on the technologies you need to get a high paying job as a web developer", + "website": "https://devworks.com", + "phone": "(111) 111-1111", + "email": "enroll@devworks.com", + "address": "233 Bay State Rd Boston MA 02215", + "careers": ["Web Development", "UI/UX", "Business"], + "housing": true, + "jobAssistance": true, + "jobGuarantee": false, + "acceptGi": true + }, + { + "_id": "5d713a66ec8f2b88b8f830b8", + "user": "5d7a514b5d2c12c7449be046", + "name": "ModernTech Bootcamp", + "description": "ModernTech has one goal, and that is to make you a rockstar developer and/or designer with a six figure salary. We teach both development and UI/UX", + "website": "https://moderntech.com", + "phone": "(222) 222-2222", + "email": "enroll@moderntech.com", + "address": "220 Pawtucket St, Lowell, MA 01854", + "careers": ["Web Development", "UI/UX", "Mobile Development"], + "housing": false, + "jobAssistance": true, + "jobGuarantee": false, + "acceptGi": true + }, + { + "_id": "5d725a037b292f5f8ceff787", + "user": "5c8a1d5b0190b214360dc031", + "name": "Codemasters", + "description": "Is coding your passion? Codemasters will give you the skills and the tools to become the best developer possible. We specialize in full stack web development and data science", + "website": "https://codemasters.com", + "phone": "(333) 333-3333", + "email": "enroll@codemasters.com", + "address": "85 South Prospect Street Burlington VT 05405", + "careers": ["Web Development", "Data Science", "Business"], + "housing": false, + "jobAssistance": false, + "jobGuarantee": false, + "acceptGi": false + }, + { + "_id": "5d725a1b7b292f5f8ceff788", + "user": "5c8a1d5b0190b214360dc032", + "name": "Devcentral Bootcamp", + "description": "Is coding your passion? Codemasters will give you the skills and the tools to become the best developer possible. We specialize in front end and full stack web development", + "website": "https://devcentral.com", + "phone": "(444) 444-4444", + "email": "enroll@devcentral.com", + "address": "45 Upper College Rd Kingston RI 02881", + "careers": [ + "Mobile Development", + "Web Development", + "Data Science", + "Business" + ], + "housing": false, + "jobAssistance": true, + "jobGuarantee": true, + "acceptGi": true + } +] diff --git a/_data/courses.json b/_data/courses.json new file mode 100644 index 0000000..4a0a634 --- /dev/null +++ b/_data/courses.json @@ -0,0 +1,101 @@ +[ + { + "_id": "5d725a4a7b292f5f8ceff789", + "title": "Front End Web Development", + "description": "This course will provide you with all of the essentials to become a successful frontend web developer. You will learn to master HTML, CSS and front end JavaScript, along with tools like Git, VSCode and front end frameworks like Vue", + "weeks": 8, + "tuition": 8000, + "minimumSkill": "beginner", + "scholarhipsAvailable": true, + "bootcamp": "5d713995b721c3bb38c1f5d0", + "user": "5d7a514b5d2c12c7449be045" + }, + { + "_id": "5d725c84c4ded7bcb480eaa0", + "title": "Full Stack Web Development", + "description": "In this course you will learn full stack web development, first learning all about the frontend with HTML/CSS/JS/Vue and then the backend with Node.js/Express/MongoDB", + "weeks": 12, + "tuition": 10000, + "minimumSkill": "intermediate", + "scholarhipsAvailable": true, + "bootcamp": "5d713995b721c3bb38c1f5d0", + "user": "5d7a514b5d2c12c7449be045" + }, + { + "_id": "5d725cb9c4ded7bcb480eaa1", + "title": "Full Stack Web Dev", + "description": "In this course you will learn all about the front end with HTML, CSS and JavaScript. You will master tools like Git and Webpack and also learn C# and ASP.NET with Postgres", + "weeks": 10, + "tuition": 12000, + "minimumSkill": "intermediate", + "scholarhipsAvailable": true, + "bootcamp": "5d713a66ec8f2b88b8f830b8", + "user": "5d7a514b5d2c12c7449be046" + }, + { + "_id": "5d725cd2c4ded7bcb480eaa2", + "title": "UI/UX", + "description": "In this course you will learn to create beautiful interfaces. It is a mix of design and development to create modern user experiences on both web and mobile", + "weeks": 12, + "tuition": 10000, + "minimumSkill": "intermediate", + "scholarhipsAvailable": true, + "bootcamp": "5d713a66ec8f2b88b8f830b8", + "user": "5d7a514b5d2c12c7449be046" + }, + { + "_id": "5d725ce8c4ded7bcb480eaa3", + "title": "Web Design & Development", + "description": "Get started building websites and web apps with HTML/CSS/JavaScript/PHP. We teach you", + "weeks": 10, + "tuition": 9000, + "minimumSkill": "beginner", + "scholarhipsAvailable": true, + "bootcamp": "5d725a037b292f5f8ceff787", + "user": "5c8a1d5b0190b214360dc031" + }, + { + "_id": "5d725cfec4ded7bcb480eaa4", + "title": "Data Science Program", + "description": "In this course you will learn Python for data science, machine learning and big data tools", + "weeks": 10, + "tuition": 12000, + "minimumSkill": "intermediate", + "scholarhipsAvailable": false, + "bootcamp": "5d725a037b292f5f8ceff787", + "user": "5c8a1d5b0190b214360dc031" + }, + { + "_id": "5d725cfec4ded7bcb480eaa5", + "title": "Web Development", + "description": "This course will teach you how to build high quality web applications with technologies like React, Node.js, PHP & Laravel", + "weeks": 8, + "tuition": 8000, + "minimumSkill": "beginner", + "scholarhipsAvailable": false, + "bootcamp": "5d725a1b7b292f5f8ceff788", + "user": "5c8a1d5b0190b214360dc032" + }, + { + "_id": "5d725cfec4ded7bcb480eaa6", + "title": "Software QA", + "description": "This course will teach you everything you need to know about quality assurance", + "weeks": 6, + "tuition": 5000, + "minimumSkill": "intermediate", + "scholarhipsAvailable": false, + "bootcamp": "5d725a1b7b292f5f8ceff788", + "user": "5c8a1d5b0190b214360dc032" + }, + { + "_id": "5d725cfec4ded7bcb480eaa7", + "title": "IOS Development", + "description": "Get started building mobile applications for IOS using Swift and other tools", + "weeks": 8, + "tuition": 6000, + "minimumSkill": "intermediate", + "scholarhipsAvailable": false, + "bootcamp": "5d725a1b7b292f5f8ceff788", + "user": "5c8a1d5b0190b214360dc032" + } +] diff --git a/_data/reviews.json b/_data/reviews.json new file mode 100644 index 0000000..4fea31e --- /dev/null +++ b/_data/reviews.json @@ -0,0 +1,66 @@ +[ + { + "_id": "5d7a514b5d2c12c7449be020", + "title": "Learned a ton!", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "8", + "bootcamp": "5d713995b721c3bb38c1f5d0", + "user": "5c8a1d5b0190b214360dc033" + }, + { + "_id": "5d7a514b5d2c12c7449be021", + "title": "Great bootcamp", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "10", + "bootcamp": "5d713995b721c3bb38c1f5d0", + "user": "5c8a1d5b0190b214360dc034" + }, + { + "_id": "5d7a514b5d2c12c7449be022", + "title": "Got me a developer job", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "7", + "bootcamp": "5d713a66ec8f2b88b8f830b8", + "user": "5c8a1d5b0190b214360dc035" + }, + { + "_id": "5d7a514b5d2c12c7449be023", + "title": "Not that great", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "4", + "bootcamp": "5d713a66ec8f2b88b8f830b8", + "user": "5c8a1d5b0190b214360dc036" + }, + { + "_id": "5d7a514b5d2c12c7449be024", + "title": "Great overall experience", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "7", + "bootcamp": "5d725a037b292f5f8ceff787", + "user": "5c8a1d5b0190b214360dc037" + }, + { + "_id": "5d7a514b5d2c12c7449be025", + "title": "Not worth the money", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "5", + "bootcamp": "5d725a037b292f5f8ceff787", + "user": "5c8a1d5b0190b214360dc038" + }, + { + "_id": "5d7a514b5d2c12c7449be026", + "title": "Best instructors", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "10", + "bootcamp": "5d725a1b7b292f5f8ceff788", + "user": "5c8a1d5b0190b214360dc039" + }, + { + "_id": "5d7a514b5d2c12c7449be027", + "title": "Was worth the investment", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", + "rating": "7", + "bootcamp": "5d725a1b7b292f5f8ceff788", + "user": "5c8a1d5b0190b214360dc040" + } +] diff --git a/_data/users.json b/_data/users.json new file mode 100644 index 0000000..f7a4c9b --- /dev/null +++ b/_data/users.json @@ -0,0 +1,107 @@ +[ + { + "_id": "5d7a514b5d2c12c7449be042", + "name": "Admin Account", + "email": "admin@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5d7a514b5d2c12c7449be043", + "name": "Publisher Account", + "email": "publisher@gmail.com", + "role": "publisher", + "password": "123456" + }, + { + "_id": "5d7a514b5d2c12c7449be044", + "name": "User Account", + "email": "user@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5d7a514b5d2c12c7449be045", + "name": "John Doe", + "email": "john@gmail.com", + "role": "publisher", + "password": "123456" + }, + { + "_id": "5d7a514b5d2c12c7449be046", + "name": "Kevin Smith", + "email": "kevin@gmail.com", + "role": "publisher", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc031", + "name": "Mary Williams", + "email": "mary@gmail.com", + "role": "publisher", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc032", + "name": "Sasha Ryan", + "email": "sasha@gmail.com", + "role": "publisher", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc033", + "name": "Greg Harris", + "email": "greg@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc034", + "name": "Derek Glover", + "email": "derek@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc035", + "name": "Stephanie Hanson", + "email": "steph@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc036", + "name": "Jerry Wiliams", + "email": "jerry@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc037", + "name": "Maggie Johnson", + "email": "maggie@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc038", + "name": "Barry Dickens", + "email": "barry@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc039", + "name": "Ryan Bolin", + "email": "ryan@gmail.com", + "role": "user", + "password": "123456" + }, + { + "_id": "5c8a1d5b0190b214360dc040", + "name": "Sara Kensing", + "email": "sara@gmail.com", + "role": "user", + "password": "123456" + } +] diff --git a/config/config.env.env b/config/config.env.env new file mode 100644 index 0000000..e6a67d3 --- /dev/null +++ b/config/config.env.env @@ -0,0 +1,21 @@ +NODE_ENV=development +PORT=5000 + +MONGO_URI= + +GEOCODER_PROVIDER=mapquest +GEOCODER_API_KEY= + +FILE_UPLOAD_PATH= ./public/uploads +MAX_FILE_UPLOAD=1000000 + +JWT_SECRET= +JWT_EXPIRE=30d +JWT_COOKIE_EXPIRE=30 + +SMTP_HOST=smtp.mailtrap.io +SMTP_PORT=2525 +SMTP_EMAIL= +SMTP_PASSWORD= +FROM_EMAIL= +FROM_NAME= \ No newline at end of file diff --git a/config/db.js b/config/db.js new file mode 100644 index 0000000..e9b3a7b --- /dev/null +++ b/config/db.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); + +const connectDB = async () => { + mongoose.set('strictQuery', false); + const conn = await mongoose.connect(process.env.MONGO_URI, { + useNewUrlParser: true, + }); + + console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline.bold); +}; + +module.exports = connectDB; diff --git a/controllers/auth.js b/controllers/auth.js new file mode 100644 index 0000000..926f560 --- /dev/null +++ b/controllers/auth.js @@ -0,0 +1,211 @@ +const crypto = require('crypto'); +const ErrorResponse = require('../utils/errorResponse'); +const asyncHandler = require('../middleware/async'); +const sendEmail = require('../utils/sendEmail'); +const User = require('../models/User'); + +//@desc Register user +//@route POST /api/v1/auth/register +//@access Public +exports.register = asyncHandler(async (req, res, next) => { + const { name, email, password, role } = req.body; + + //Create user + const user = await User.create({ + name, + password, + email, + role, + }); + + sendTokenResponse(user, 200, res); +}); + +//@desc Login user +//@route POST /api/v1/auth/login +//@access Public +exports.login = asyncHandler(async (req, res, next) => { + const { email, password } = req.body; + + //Validate email and password + if (!email || !password) { + return next(new ErrorResponse('Please provide an email and password', 400)); + } + + //Check for user + const user = await User.findOne({ email }).select('+password'); + + if (!user) { + return next(new ErrorResponse('Invalid credentials', 401)); + } + + //Check if password matches + const isMatch = await user.matchPassword(password); + + if (!isMatch) { + return next(new ErrorResponse('Invalid credentials', 401)); + } + + sendTokenResponse(user, 200, res); +}); + +//@desc Log user out / clear cookie +//@route GET /api/v1/auth/logout +//@access Private +exports.logout = asyncHandler(async (req, res, next) => { + res.cookie('token', 'none', { + expires: new Date(Date.now() + 10 * 1000), + httpOnly: true, + }); + + res.status(200).json({ + success: true, + data: {}, + }); +}); + +//@desc Get current logged in user +//@route POST /api/v1/auth/me +//@access Private +exports.getMe = asyncHandler(async (req, res, next) => { + const user = await User.findById(req.user.id); + + res.status(200).json({ + success: true, + data: user, + }); +}); + +//@desc Update user details +//@route PUT /api/v1/auth/updatedetails +//@access Private +exports.updateDetails = asyncHandler(async (req, res, next) => { + const fieldsToUpdate = { + name: req.body.name, + email: req.body.email, + }; + + const user = await User.findByIdAndUpdate(req.user.id, fieldsToUpdate, { + new: true, + runValidators: true, + }); + + res.status(200).json({ + success: true, + data: user, + }); +}); + +//@desc Update password +//@route PUT /api/v1/auth/updatepassword +//@access Private +exports.updatePassword = asyncHandler(async (req, res, next) => { + const user = await User.findById(req.user.id).select('+password'); + + //Check current password + if (!(await user.matchPassword(req.body.currentPassword))) { + return next(new ErrorResponse('Password is incorrect', 401)); + } + + user.password = req.body.newPassword; + await user.save(); + + sendTokenResponse(user, 200, res); +}); + +//@desc Frogot password +//@route POST /api/v1/auth/forgotpassword +//@access Public +exports.forgotPassword = asyncHandler(async (req, res, next) => { + const user = await User.findOne({ email: req.body.email }); + + if (!user) { + return next(new ErrorResponse('There is no user with that email', 404)); + } + + //Get reset token + const resetToken = user.getResetPasswordToken(); + + await user.save({ validateBeforeSave: false }); + + //Create reset url + const resetUrl = `${req.protocol}://${req.get( + 'host' + )}/api/v1/auth/resetpassword/${resetToken}`; + + const message = `You are receiving this email because you (or someone else) + has requested the reset of a password. Please make a PUT request to: \n\n ${resetUrl}`; + + try { + await sendEmail({ + email: user.email, + subject: 'Password reset token', + message, + }); + + res.status(200).json({ success: true, data: 'Email sent' }); + } catch (err) { + console.log(err); + user.resetPasswordToken = undefined; + user.resetPasswordExpire = undefined; + + await user.save({ validateBeforeSave: false }); + + return next(new ErrorResponse('Email could not be sent', 500)); + } + + res.status(200).json({ + success: true, + data: user, + }); +}); + +//@desc Reset password +//@route PUT /api/v1/auth/resetpassword/:resettoken +//@access Public +exports.resetPassword = asyncHandler(async (req, res, next) => { + //Get hashed token + const resetPasswordToken = crypto + .createHash('sha256') + .update(req.params.resettoken) + .digest('hex'); + + const user = await User.findOne({ + resetPasswordToken, + resetPasswordExpire: { $gt: Date.now() }, + }); + + if (!user) { + return next(new ErrorResponse('Invalid token', 400)); + } + + //Set new password + user.password = req.body.password; + user.resetPasswordToken = undefined; + user.resetPasswordExpire = undefined; + await user.save(); + + sendTokenResponse(user, 200, res); +}); + +//Get token from model, create cookie and send response +const sendTokenResponse = (user, statusCode, res) => { + //Create token + const token = user.getSignedJwtToken(); + + const options = { + expires: new Date( + Date.now() + process.env.JWT_COOKIE_EXPIRE * 24 * 60 * 60 * 1000 + ), + httpOnly: true, + }; + + if (process.env.NODE_ENV === 'production') { + options.secure = true; + } + + res.status(statusCode).cookie('token', token, options).json({ + success: true, + token, + }); +}; diff --git a/controllers/bootcamps.js b/controllers/bootcamps.js new file mode 100644 index 0000000..337ad13 --- /dev/null +++ b/controllers/bootcamps.js @@ -0,0 +1,206 @@ +const path = require('path'); +const ErrorResponse = require('../utils/errorResponse'); +const asyncHandler = require('../middleware/async'); +const geocoder = require('../utils/geocoder'); +const Bootcamp = require('../models/Bootcamp'); + +//@desc Get all bootcamps +//@route GET /api/v1/bootcamps +//@access Public + +exports.getBootcamps = asyncHandler(async (req, res, next) => { + res.status(200).json(res.advancedResults); +}); + +//@desc Get single bootcamp +//@route GET /api/v1/bootcamps/:id +//@access Public + +exports.getBootcamp = asyncHandler(async (req, res, next) => { + const bootcamp = await Bootcamp.findById(req.params.id); + + if (!bootcamp) { + return next( + new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) + ); + } + + res.status(200).json({ success: true, data: bootcamp }); +}); + +//@desc Create new bootcamp +//@route POST /api/v1/bootcamps +//@access Private +exports.createBootcamp = asyncHandler(async (req, res, next) => { + //Add user to req.body + req.body.user = req.user.id; + + //Check for published bootcamp + const publishedBootcamp = await Bootcamp.findOne({ user: req.user.id }); + + //If the user is not an admin, they can only add one bootcamp + if (publishedBootcamp && req.user.role !== 'admin') { + return next( + new ErrorResponse( + `The user with ID ${req.user.id} has already published a bootcamp`, + 400 + ) + ); + } + + const bootcamp = await Bootcamp.create(req.body); + + res.status(201).json({ + success: true, + data: bootcamp, + }); +}); + +//@desc Update bootcamp +//@route PUT /api/v1/bootcamps:id +//@access Private + +exports.updateBootcamp = asyncHandler(async (req, res, next) => { + let bootcamp = await Bootcamp.findById(req.params.id); + + if (!bootcamp) { + return next( + new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) + ); + } + + //Make sure user is bootcamp owner + if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next( + new ErrorResponse( + `User ${req.params.id} is not authorized to update this bootcamp`, + 401 + ) + ); + } + + bootcamp = await Bootcamp.findOneAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }); + + res.status(200).json({ success: true, data: bootcamp }); +}); + +//@desc Delete bootcamp +//@route DELETE /api/v1/bootcamps/:id +//@access Private + +exports.deleteBootcamp = asyncHandler(async (req, res, next) => { + const bootcamp = await Bootcamp.findById(req.params.id, req.body); + + if (!bootcamp) { + return next( + new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) + ); + } + + //Make sure user is bootcamp owner + if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next( + new ErrorResponse( + `User ${req.params.id} is not authorized to delete this bootcamp`, + 401 + ) + ); + } + + bootcamp.remove(); + + res.status(200).json({ success: true, data: {} }); +}); + +//@desc Get bootcamp within a radius +//@route Get /api/v1/bootcamps/radius/:zipcode/:distance +//@access Private + +exports.getBootcampsInRadius = asyncHandler(async (req, res, next) => { + const { zipcode, distance } = req.params; + + //Get lat/lng from Geocoder + const loc = await geocoder.geocode(zipcode); + const lat = loc[0].latitude; + const lng = loc[0].longitude; + + //Calc radius using radians + //Divide dest by radius of Earth + //Earth radius = 3,963 mi / 6.378 km + const radius = distance / 6378; + + const bootcamps = await Bootcamp.find({ + location: { $geoWithin: { $centerSphere: [[lng, lat], radius] } }, + }); + + res.status(200).json({ + success: true, + count: bootcamps.length, + data: bootcamps, + }); +}); + +//@desc Upload photo for bootcamp +//@route PUT /api/v1/bootcamps/:id/photo +//@access Private + +exports.bootcampPhotoUpload = asyncHandler(async (req, res, next) => { + const bootcamp = await Bootcamp.findById(req.params.id, req.body); + + if (!bootcamp) { + return next( + new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) + ); + } + + //Make sure user is bootcamp owner + if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next( + new ErrorResponse( + `User ${req.params.id} is not authorized to update this bootcamp`, + 401 + ) + ); + } + + if (!req.files) { + return next(new ErrorResponse(`Please upload a file`, 400)); + } + + const file = req.files.file; + + //Make sure the image is a photo + if (!file.mimetype.startsWith('image')) { + return next(new ErrorResponse(`Please upload an image file`, 400)); + } + + //Check file size + if (file.size > process.env.MAX_FILE_UPLOAD) { + return next( + new ErrorResponse( + `Please upload an image less than ${process.env.MAX_FILE_UPLOAD}`, + 400 + ) + ); + } + + //Create custom file name + file.name = `photo_${bootcamp._id}${path.parse(file.name).ext}`; + + file.mv(`${process.env.FILE_UPLOAD_PATH}/${file.name}`, async (err) => { + if (err) { + console.error(err); + return next(new ErrorResponse(`Problem with file upload`, 500)); + } + + await Bootcamp.findByIdAndUpdate(req.params.id, { photo: file.name }); + + res.status(200).json({ + success: true, + data: file.name, + }); + }); +}); diff --git a/controllers/courses.js b/controllers/courses.js new file mode 100644 index 0000000..9424c6a --- /dev/null +++ b/controllers/courses.js @@ -0,0 +1,145 @@ +const ErrorResponse = require('../utils/errorResponse'); +const asyncHandler = require('../middleware/async'); +const Course = require('../models/Course'); +const Bootcamp = require('../models/Bootcamp'); + +//@desc Get courses +//@route GET /api/v1/courses +//@route GET /api/v1/bootcamp/:bootcampId/courses +//@access Public +exports.getCourses = asyncHandler(async (req, res, next) => { + if (req.params.bootcampId) { + const courses = await Course.find({ bootcamp: req.params.bootcampId }); + + return res.status(200).json({ + success: true, + count: courses.length, + data: courses, + }); + } else { + res.status(200).json(res.advancedResults); + } +}); + +//@desc Get single course +//@route GET /api/v1/courses/:id +//@access Public +exports.getCourse = asyncHandler(async (req, res, next) => { + const course = await Course.findById(req.params.id).populate({ + path: 'bootcamp', + select: 'name description', + }); + + if (!course) { + return next( + new ErrorResponse(`No course with the id of ${req.params.id}`), + 404 + ); + } + + res.status(200).json({ + success: true, + data: course, + }); +}); + +//@desc Add course +//@route POST /api/v1/bootcamps/:bootcampId/courses +//@access Private +exports.addCourse = asyncHandler(async (req, res, next) => { + req.body.bootcamp = req.params.bootcampId; + req.body.user = req.user.id; + + const bootcamp = await Bootcamp.findById(req.params.bootcampId); + + if (!bootcamp) { + return next( + new ErrorResponse(`No bootcamp with the id of ${req.params.bootcampId}`), + 404 + ); + } + + //Make sure user is bootcamp owner + if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next( + new ErrorResponse( + `User ${req.user.id} is not authorized to add a course to bootcamp ${bootcamp._id}`, + 401 + ) + ); + } + + const course = await Course.create(req.body); + + res.status(200).json({ + success: true, + data: course, + }); +}); + +//@desc Update course +//@route PUT /api/v1/courses/:id +//@access Private +exports.updateCourse = asyncHandler(async (req, res, next) => { + let course = await Course.findById(req.params.id); + + if (!course) { + return next( + new ErrorResponse(`No course with the id of ${req.params.id}`), + 404 + ); + } + + //Make sure user is course owner + if (course.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next( + new ErrorResponse( + `User ${req.user.id} is not authorized to update course ${course._id}`, + 401 + ) + ); + } + + course = await Course.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }); + + await course.save(); + + res.status(200).json({ + success: true, + data: course, + }); +}); + +//@desc Delete course +//@route DELETE /api/v1/courses/:id +//@access Private +exports.deleteCourse = asyncHandler(async (req, res, next) => { + const course = await Course.findById(req.params.id); + + if (!course) { + return next( + new ErrorResponse(`No course with the id of ${req.params.id}`), + 404 + ); + } + + //Make sure user is course owner + if (course.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next( + new ErrorResponse( + `User ${req.user.id} is not authorized to delete course ${course._id}`, + 401 + ) + ); + } + + await course.remove(); + + res.status(200).json({ + success: true, + data: {}, + }); +}); diff --git a/controllers/reviews.js b/controllers/reviews.js new file mode 100644 index 0000000..d7ede4e --- /dev/null +++ b/controllers/reviews.js @@ -0,0 +1,124 @@ +const ErrorResponse = require('../utils/errorResponse'); +const asyncHandler = require('../middleware/async'); +const Review = require('../models/Review'); +const Bootcamp = require('../models/Bootcamp'); + +//@desc Get reviews +//@route GET /api/v1/reviews +//@route GET /api/v1/bootcamp/:bootcampId/reviews +//@access Public +exports.getReviews = asyncHandler(async (req, res, next) => { + if (req.params.bootcampId) { + const reviews = await Review.find({ bootcamp: req.params.bootcampId }); + + return res.status(200).json({ + success: true, + count: reviews.length, + data: reviews, + }); + } else { + res.status(200).json(res.advancedResults); + } +}); + +//@desc Get single review +//@route GET /api/v1/reviews/:id +//@access Public +exports.getReview = asyncHandler(async (req, res, next) => { + const review = await Review.findById(req.params.id).populate({ + path: 'bootcamp', + select: 'name description', + }); + + if (!review) { + return next( + new ErrorResponse(`No review found with the id of ${req.params.id}`, 404) + ); + } + + res.status(200).json({ + success: true, + data: review, + }); +}); + +//@desc Add review +//@route POST /api/v1/bootcamps/:bootcampId/reviews +//@access Private +exports.addReview = asyncHandler(async (req, res, next) => { + req.body.bootcamp = req.params.bootcampId; + req.body.user = req.user.id; + + const bootcamp = await Bootcamp.findById(req.params.bootcampId); + + if (!bootcamp) { + return next( + new ErrorResponse( + `No bootcamp with the id of ${req.params.bootcampId}`, + 404 + ) + ); + } + + const review = await Review.create(req.body); + + res.status(201).json({ + success: true, + data: review, + }); +}); + +//@desc Update review +//@route PUT /api/v1/reviews/:id +//@access Private +exports.updateReview = asyncHandler(async (req, res, next) => { + let review = await Review.findById(req.params.id); + + if (!review) { + return next( + new ErrorResponse(`No review with the id of ${req.params.id}`, 404) + ); + } + + //Make sure review belongs to user or user is an admin + if (review.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next(new ErrorResponse(`Not authorized to update review`, 401)); + } + + review = await Review.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }); + + await review.save(); + + res.status(200).json({ + success: true, + data: review, + }); +}); + +//@desc Delete review +//@route DELETE /api/v1/reviews/:id +//@access Private +exports.deleteReview = asyncHandler(async (req, res, next) => { + const review = await Review.findById(req.params.id); + + if (!review) { + return next( + new ErrorResponse(`No review with the id of ${req.params.id}`, 404) + ); + } + + //Make sure review belongs to user or user is an admin + if (review.user.toString() !== req.user.id && req.user.role !== 'admin') { + return next(new ErrorResponse(`Not authorized to update review`, 401)); + } + + await review.remove(); + + res.status(200).json({ + success: true, + data: {}, + }); +}); diff --git a/controllers/users.js b/controllers/users.js new file mode 100644 index 0000000..01cd85d --- /dev/null +++ b/controllers/users.js @@ -0,0 +1,61 @@ +const ErrorResponse = require('../utils/errorResponse'); +const asyncHandler = require('../middleware/async'); +const User = require('../models/User'); + +//@desc Get all users +//@route GET /api/v1/auth/users +//@access Private/admin +exports.getUsers = asyncHandler(async (req, res, next) => { + res.status(200).json(res.advancedResults); +}); + +//@desc Get single user +//@route GET /api/v1/auth/users/:id +//@access Private/admin +exports.getUser = asyncHandler(async (req, res, next) => { + const user = await User.findById(req.params.id); + + res.status(200).json({ + success: true, + data: user, + }); +}); + +//@desc Create user +//@route POST /api/v1/auth/users +//@access Private/admin +exports.createUser = asyncHandler(async (req, res, next) => { + const user = await User.create(req.body); + + res.status(201).json({ + success: true, + data: user, + }); +}); + +//@desc Update user +//@route PUT /api/v1/auth/users/:id +//@access Private/admin +exports.updateUser = asyncHandler(async (req, res, next) => { + const user = await User.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }); + + res.status(200).json({ + success: true, + data: user, + }); +}); + +//@desc Delete user +//@route DELETE /api/v1/auth/users/:id +//@access Private/admin +exports.deleteUser = asyncHandler(async (req, res, next) => { + await User.findByIdAndDelete(req.params.id); + + res.status(200).json({ + success: true, + data: {}, + }); +}); diff --git a/middleware/advancedResults.js b/middleware/advancedResults.js new file mode 100644 index 0000000..5aef033 --- /dev/null +++ b/middleware/advancedResults.js @@ -0,0 +1,81 @@ +const advancedResults = (model, populate) => async (req, res, next) => { + let query; + + // Copy req.query + const reqQuery = { ...req.query }; + + //Fields to exclude + const removeFields = ['select', 'sort', 'page', 'limit']; + + //Loop over removeFields and delete them from reqQuery + removeFields.forEach((param) => delete reqQuery[param]); + + //Create query string + let queryStr = JSON.stringify(reqQuery); + + //Create operator like ($gt, $gte, etc) + queryStr = queryStr.replace( + /\b(gt|gte|lt|lte|in)\b/g, + (match) => `$${match}` + ); + + //Finding resource + query = model.find(JSON.parse(queryStr)); + + //Select fields + if (req.query.select) { + const fields = req.query.select.split(',').join(' '); + query = query.select(fields); + } + + // Sort + if (req.query.sort) { + const sortBy = req.query.sort.split(',').join(' '); + query = query.sort(sortBy); + } else { + query = query.sort('-createdAt'); + } + + // Pagination + const page = parseInt(req.query.page, 10) || 1; + const limit = parseInt(req.query.limit, 10) || 25; + const startIndex = (page - 1) * limit; + const endIndex = page * limit; + const total = await model.countDocuments(); + + query = query.skip(startIndex).limit(limit); + + if (populate) { + query = query.populate(populate); + } + + //Executing query + const results = await query; + + // Pagination result + const pagination = {}; + + if (endIndex < total) { + pagination.next = { + page: page + 1, + limit, + }; + } + + if (startIndex > 0) { + pagination.prev = { + page: page - 1, + limit, + }; + } + res.advancedResults = { + success: true, + count: results.length, + pagination, + data: results, + }; + + next(); +}; + +module.exports = advancedResults; diff --git a/middleware/async.js b/middleware/async.js new file mode 100644 index 0000000..126a578 --- /dev/null +++ b/middleware/async.js @@ -0,0 +1,4 @@ +const asyncHandler = (fn) => (req, res, next) => + Promise.resolve(fn(req, res, next)).catch(next); + +module.exports = asyncHandler; diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..8fb0fb9 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,54 @@ +const jwt = require('jsonwebtoken'); +const asyncHandler = require('./async'); +const ErrorResponse = require('../utils/errorResponse'); +const User = require('../models/User'); + +// Protect routes +exports.protect = asyncHandler(async (req, res, next) => { + let token; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith('Bearer') + ) { + // Set token from Bearer token in header + token = req.headers.authorization.split(' ')[1]; + // Set token from cookie + } + // else if (req.cookies.token) { + // token = req.cookies.token; + // } + + //Make sure token exists + if (!token) { + return next(new ErrorResponse('Not authorized to access this route', 401)); + } + + try { + //Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + console.log(decoded); + + req.user = await User.findById(decoded.id); + + next(); + } catch (err) { + return next(new ErrorResponse('Not authorized to access this route', 401)); + } +}); + +//Grant access to specific roles +exports.authorize = (...roles) => { + return (req, res, next) => { + if (!roles.includes(req.user.role)) { + return next( + new ErrorResponse( + `User role ${req.user.role} is not authorized to acces this route`, + 403 + ) + ); + } + next(); + }; +}; diff --git a/middleware/error.js b/middleware/error.js new file mode 100644 index 0000000..db25a85 --- /dev/null +++ b/middleware/error.js @@ -0,0 +1,35 @@ +const ErrorResponse = require('../utils/errorResponse'); + +const errorHandler = (err, req, res, next) => { + let error = { ...err }; + + error.message = err.message; + + // Log to console for dev + console.log(err); + + // Mongoose bad ObjectId + if (err.name === 'CastError') { + const message = `Resource not found`; + error = new ErrorResponse(message, 404); + } + + // Mongoose duplicate key + if (err.code === 11000) { + const message = 'Duplicate field value entered'; + error = new ErrorResponse(message, 400); + } + + //Mongoose validation error + if (err.name === 'ValidationError') { + const message = Object.values(err.errors).map((val) => val.message); + error = new ErrorResponse(message, 400); + } + + res.status(error.statusCode || 500).json({ + success: false, + error: error.message || 'Server Error', + }); +}; + +module.exports = errorHandler; diff --git a/middleware/logger.js b/middleware/logger.js new file mode 100644 index 0000000..108f66a --- /dev/null +++ b/middleware/logger.js @@ -0,0 +1,9 @@ +//@desc Logs request to console +const logger = (req, res, next) => { + console.log( + `${req.method} ${req.protocol}://${req.get('host')}${req.originalUrl}` + ); + next(); +}; + +module.exports = logger; diff --git a/models/Bootcamp.js b/models/Bootcamp.js new file mode 100644 index 0000000..24c5c00 --- /dev/null +++ b/models/Bootcamp.js @@ -0,0 +1,156 @@ +const mongoose = require('mongoose'); +const slugify = require('slugify'); +const geocoder = require('../utils/geocoder'); + +const BootcampSchema = new mongoose.Schema( + { + name: { + type: String, + required: [true, 'Please add a name'], + unique: true, + trim: true, + maxlenght: [50, 'Name can not ba more than 50 characters'], + }, + slug: String, + description: { + type: String, + required: [true, 'Please add a description'], + maxlenght: [500, 'Description can not ba more than 500 characters'], + }, + website: { + type: String, + match: [ + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/, + 'Please use a valid URL with HTTP or HTTPS', + ], + }, + phone: { + type: String, + maxlenght: [20, 'Phone can not ba more than 20 characters'], + }, + email: { + type: String, + match: [ + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + 'Please add a valid email', + ], + }, + address: { + type: String, + required: [true, 'Please add an address'], + }, + location: { + // GeoJSON Point + type: { + type: String, + enum: ['Point'], + //required: true, + }, + coordinates: { + type: [Number], + //required: true, + index: '2dsphere', + }, + formattedAddress: String, + street: String, + city: String, + state: String, + zipcode: String, + counry: String, + }, + careers: { + //Array of strings + type: [String], + required: true, + enum: [ + 'Web Development', + 'Mobile Development', + 'UI/UX', + 'Data Science', + 'Business', + 'Other', + ], + }, + averageRating: { + type: Number, + min: [1, 'Rating must be at least 1'], + max: [10, 'Rating can not be more than 10'], + }, + averageCost: Number, + photo: { + type: String, + default: 'no-photo.jpg', + }, + housing: { + type: Boolean, + default: false, + }, + jobAssistance: { + type: Boolean, + default: false, + }, + jobGuarantee: { + type: Boolean, + default: false, + }, + acceptGi: { + type: Boolean, + default: false, + }, + createdAt: { + type: Date, + default: Date.now, + }, + user: { + type: mongoose.Schema.ObjectId, + ref: 'User', + required: true, + }, + }, + { + toJSON: { virtuals: true }, + toObject: { virtuals: true }, + } +); + +// Create Bootcamp slug from the name +BootcampSchema.pre('save', function (next) { + this.slug = slugify(this.name, { lower: true }); + next(); +}); + +// Geocode & create location field +BootcampSchema.pre('save', async function (next) { + const loc = await geocoder.geocode(this.address); + this.location = { + type: 'Point', + coordinates: [loc[0].longitude, loc[0].latitude], + formattedAddress: loc[0].formattedAddress, + street: loc[0].streetName, + city: loc[0].city, + state: loc[0].stateCode, + zipcode: loc[0].zipcode, + country: loc[0].countryCode, + }; + + // Do not save address in DB + this.address = undefined; + next(); +}); + +//Cascade delete courses when a bootcamp is deleted +BootcampSchema.pre('remove', async function (next) { + console.log(`Courses being removed from bootcamp ${this._id}`); + await this.model('Course').deleteMany({ bootcamp: this._id }); + next(); +}); + +//Reverse populate with virtuals +BootcampSchema.virtual('courses', { + ref: 'Course', + localField: '_id', + foreignField: 'bootcamp', + justOne: false, +}); + +module.exports = mongoose.model('Bootcamp', BootcampSchema); diff --git a/models/Course.js b/models/Course.js new file mode 100644 index 0000000..069c97e --- /dev/null +++ b/models/Course.js @@ -0,0 +1,84 @@ +const mongoose = require('mongoose'); + +const CourseSchema = new mongoose.Schema({ + title: { + type: String, + trim: true, + required: [true, 'Please add a course title'], + }, + description: { + type: String, + required: [true, 'Please add a course description'], + }, + weeks: { + type: String, + required: [true, 'Please add number of weeks'], + }, + tuition: { + type: Number, + required: [true, 'Please add a tuition cost'], + }, + minimumSkill: { + type: String, + required: [true, 'Please add a minimum skill'], + enum: ['beginner', 'intermediate', 'advanced'], + }, + scholarshipAvailable: { + type: Boolean, + default: false, + }, + createdAt: { + type: Date, + default: Date.now, + }, + bootcamp: { + type: mongoose.Schema.ObjectId, + ref: 'Bootcamp', + required: true, + }, + user: { + type: mongoose.Schema.ObjectId, + ref: 'User', + required: true, + }, +}); + +//Static method to get avg of course tuitions +CourseSchema.statics.getAverageCost = async function (bootcampId) { + const obj = await this.aggregate([ + { + $match: { bootcamp: bootcampId }, + }, + { + $group: { + _id: '$bootcamp', + averageCost: { $avg: '$tuition' }, + }, + }, + ]); + + try { + await this.model('Bootcamp').findByIdAndUpdate( + bootcampId, + obj[0] + ? { + averageCost: Math.ceil(obj[0].averageCost), + } + : { averageCost: undefined } + ); + } catch (err) { + console.error(err); + } +}; + +//Call getAverageCost after save +CourseSchema.post('save', function () { + this.constructor.getAverageCost(this.bootcamp); +}); + +//Call getAverageCost before remove +CourseSchema.post('remove', function () { + this.constructor.getAverageCost(this.bootcamp); +}); + +module.exports = mongoose.model('Course', CourseSchema); diff --git a/models/Review.js b/models/Review.js new file mode 100644 index 0000000..08fbee9 --- /dev/null +++ b/models/Review.js @@ -0,0 +1,72 @@ +const mongoose = require('mongoose'); + +const ReviewSchema = new mongoose.Schema({ + title: { + type: String, + trim: true, + required: [true, 'Please add a title for the review'], + maxlength: 100, + }, + text: { + type: String, + required: [true, 'Please add some text'], + }, + rating: { + type: Number, + min: 1, + max: 10, + required: [true, 'Please add a rating between 1 and 10'], + }, + createdAt: { + type: Date, + default: Date.now, + }, + bootcamp: { + type: mongoose.Schema.ObjectId, + ref: 'Bootcamp', + required: true, + }, + user: { + type: mongoose.Schema.ObjectId, + ref: 'User', + required: true, + }, +}); + +//Prevent user from submitting more than one review per bootcamp +ReviewSchema.index({ bootcamp: 1, user: 1 }, { unique: true }); + +//Static method to get avg rating and save +ReviewSchema.statics.getAverageRating = async function (bootcampId) { + const obj = await this.aggregate([ + { + $match: { bootcamp: bootcampId }, + }, + { + $group: { + _id: '$bootcamp', + averageRating: { $avg: '$rating' }, + }, + }, + ]); + + try { + await this.model('Bootcamp').findByIdAndUpdate(bootcampId, { + averageRating: Math.ceil(obj[0].averageRating), + }); + } catch (err) { + console.error(err); + } +}; + +//Call getAverageCost after save +ReviewSchema.post('save', function () { + this.constructor.getAverageRating(this.bootcamp); +}); + +//Call getAverageCost before remove +ReviewSchema.post('remove', function () { + this.constructor.getAverageRating(this.bootcamp); +}); + +module.exports = mongoose.model('Review', ReviewSchema); diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..d882483 --- /dev/null +++ b/models/User.js @@ -0,0 +1,78 @@ +const crypto = require('crypto'); +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); + +const UserSchema = new mongoose.Schema({ + name: { + type: String, + required: [true, 'Please add a name'], + }, + email: { + type: String, + required: [true, 'Please add an email'], + unique: true, + match: [ + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + 'Please add a valid email', + ], + }, + role: { + type: String, + enum: ['user', 'publisher'], + default: 'user', + }, + password: { + type: String, + required: [true, 'Please add a password'], + minlength: 6, + select: false, + }, + resetPasswordToken: String, + resetPasswordExpire: Date, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +//Encrypt password using bcrypt +UserSchema.pre('save', async function (next) { + if (!this.isModified('password')) { + next(); + } + + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); +}); + +//Sign JWT and return +UserSchema.methods.getSignedJwtToken = function () { + return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { + expiresIn: process.env.JWT_EXPIRE, + }); +}; + +//Match user entered password to hashed password in database +UserSchema.methods.matchPassword = async function (enteredPassword) { + return await bcrypt.compare(enteredPassword, this.password); +}; + +// Generate and hash password token +UserSchema.methods.getResetPasswordToken = function () { + //Generate token + const resetToken = crypto.randomBytes(20).toString('hex'); + + //Hash token and set to resetPasswordToken field + this.resetPasswordToken = crypto + .createHash('sha256') + .update(resetToken) + .digest('hex'); + + //Set expire 10 min + this.resetPasswordExpire = Date.now() + 10 * 60 * 1000; + + return resetToken; +}; + +module.exports = mongoose.model('User', UserSchema); diff --git a/package-lock.json b/package-lock.json index 4bf3fcb..059156b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,1114 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "bcryptjs": "^2.4.3", + "colors": "^1.4.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", "dotenv": "^16.0.3", - "express": "^4.18.2" + "express": "^4.18.2", + "express-fileupload": "^1.4.0", + "express-mongo-sanitize": "^2.2.0", + "express-rate-limit": "^6.7.0", + "helmet": "^6.0.1", + "hpp": "^0.2.3", + "jsonwebtoken": "^9.0.0", + "mongoose": "^6.8.2", + "morgan": "^1.10.0", + "node-geocoder": "^4.2.0", + "nodemailer": "^6.9.0", + "slugify": "^1.6.5", + "xss-clean": "^0.1.1" }, "devDependencies": { "nodemon": "^2.0.20" } }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", + "integrity": "sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==", + "optional": true, + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "optional": true + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz", + "integrity": "sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==", + "optional": true, + "dependencies": { + "@aws-crypto/ie11-detection": "^2.0.0", + "@aws-crypto/sha256-js": "^2.0.0", + "@aws-crypto/supports-web-crypto": "^2.0.0", + "@aws-crypto/util": "^2.0.0", + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "optional": true + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz", + "integrity": "sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==", + "optional": true, + "dependencies": { + "@aws-crypto/util": "^2.0.0", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "optional": true + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz", + "integrity": "sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==", + "optional": true, + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "optional": true + }, + "node_modules/@aws-crypto/util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.2.tgz", + "integrity": "sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "^3.110.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "optional": true + }, + "node_modules/@aws-sdk/abort-controller": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.226.0.tgz", + "integrity": "sha512-cJVzr1xxPBd08voknXvR0RLgtZKGKt6WyDpH/BaPCu3rfSqWCDZKzwqe940eqosjmKrxC6pUZNKASIqHOQ8xxQ==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.241.0.tgz", + "integrity": "sha512-9X/MwcnSwWfB0ijggFjyBWa4gtlUAyI39eBaVSE0AxMcgLlHKedEK6w5F1RrtvWqb7KyJDsyAysVecU4E9zQQQ==", + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.241.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-node": "3.241.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-signing": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.241.0.tgz", + "integrity": "sha512-Jm4HR+RYAqKMEYZvvWaq0NYUKKonyInOeubObXH4BLXZpmUBSdYCSjjLdNJY3jkQoxbDVPVMIurVNh5zT5SMRw==", + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.241.0.tgz", + "integrity": "sha512-/Ml2QBGpGfUEeBrPzBZhSTBkHuXFD2EAZEIHGCBH4tKaURDI6/FoGI8P1Rl4BzoFt+II/Cr91Eox6YT9EwChsQ==", + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.241.0.tgz", + "integrity": "sha512-vmlG8cJzRf8skCtTJbA2wBvD2c3NQ5gZryzJvTKDS06KzBzcEpnjlLseuTekcnOiRNekbFUX5hRu5Zj3N2ReLg==", + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-node": "3.241.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-sdk-sts": "3.226.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-signing": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "fast-xml-parser": "4.0.11", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/config-resolver": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.234.0.tgz", + "integrity": "sha512-uZxy4wzllfvgCQxVc+Iqhde0NGAnfmV2hWR6ejadJaAFTuYNvQiRg9IqJy3pkyDPqXySiJ8Bom5PoJfgn55J/A==", + "optional": true, + "dependencies": { + "@aws-sdk/signature-v4": "3.226.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-config-provider": "3.208.0", + "@aws-sdk/util-middleware": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.241.0.tgz", + "integrity": "sha512-e2hlXWG9DH93uVe2wHIUrUOrgZTLzCV3gBd10D3/usSzS4FvVVU7OmidnRPYCLLnt3EvnL5b4REOedO1q8hv8g==", + "optional": true, + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.241.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.226.0.tgz", + "integrity": "sha512-sd8uK1ojbXxaZXlthzw/VXZwCPUtU3PjObOfr3Evj7MPIM2IH8h29foOlggx939MdLQGboJf9gKvLlvKDWtJRA==", + "optional": true, + "dependencies": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-imds": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.226.0.tgz", + "integrity": "sha512-//z/COQm2AjYFI1Lb0wKHTQSrvLFTyuKLFQGPJsKS7DPoxGOCKB7hmYerlbl01IDoCxTdyL//TyyPxbZEOQD5Q==", + "optional": true, + "dependencies": { + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.241.0.tgz", + "integrity": "sha512-CI+mu6h74Kzmscw35TvNkc/wYHsHPGAwP7humSHoWw53H9mVw21Ggft/dT1iFQQZWQ8BNXkzuXlNo1IlqwMgOA==", + "optional": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.226.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/credential-provider-process": "3.226.0", + "@aws-sdk/credential-provider-sso": "3.241.0", + "@aws-sdk/credential-provider-web-identity": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.241.0.tgz", + "integrity": "sha512-08zPQcD5o9brQmzEipWHeHgU85aQcEF8MWLfpeyjO6e1/l7ysQ35NsS+PYtv77nLpGCx/X+ZuW/KXWoRrbw77w==", + "optional": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.226.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/credential-provider-ini": "3.241.0", + "@aws-sdk/credential-provider-process": "3.226.0", + "@aws-sdk/credential-provider-sso": "3.241.0", + "@aws-sdk/credential-provider-web-identity": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.226.0.tgz", + "integrity": "sha512-iUDMdnrTvbvaCFhWwqyXrhvQ9+ojPqPqXhwZtY1X/Qaz+73S9gXBPJHZaZb2Ke0yKE1Ql3bJbKvmmxC/qLQMng==", + "optional": true, + "dependencies": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.241.0.tgz", + "integrity": "sha512-6Bjd6eEIrVomRTrPrM4dlxusQm+KMJ9hLYKECCpFkwDKIK+pTgZNLRtQdalHyzwneHJPdimrm8cOv1kUQ8hPoA==", + "optional": true, + "dependencies": { + "@aws-sdk/client-sso": "3.241.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/token-providers": "3.241.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.226.0.tgz", + "integrity": "sha512-CCpv847rLB0SFOHz2igvUMFAzeT2fD3YnY4C8jltuJoEkn0ITn1Hlgt13nTJ5BUuvyti2mvyXZHmNzhMIMrIlw==", + "optional": true, + "dependencies": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.241.0.tgz", + "integrity": "sha512-J3Q45t1o35OhUI6gWks7rmosPT+mFWXiaHl2LST509Ovjwx6SFs2PvbGP6n7xqUzxyq5Rk6FzZBwB8ItuAa6Qw==", + "optional": true, + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.241.0", + "@aws-sdk/client-sso": "3.241.0", + "@aws-sdk/client-sts": "3.241.0", + "@aws-sdk/credential-provider-cognito-identity": "3.241.0", + "@aws-sdk/credential-provider-env": "3.226.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/credential-provider-ini": "3.241.0", + "@aws-sdk/credential-provider-node": "3.241.0", + "@aws-sdk/credential-provider-process": "3.226.0", + "@aws-sdk/credential-provider-sso": "3.241.0", + "@aws-sdk/credential-provider-web-identity": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/fetch-http-handler": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.226.0.tgz", + "integrity": "sha512-JewZPMNEBXfi1xVnRa7pVtK/zgZD8/lQ/YnD8pq79WuMa2cwyhDtr8oqCoqsPW+WJT5ScXoMtuHxN78l8eKWgg==", + "optional": true, + "dependencies": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/querystring-builder": "3.226.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/hash-node": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.226.0.tgz", + "integrity": "sha512-MdlJhJ9/Espwd0+gUXdZRsHuostB2WxEVAszWxobP0FTT9PnicqnfK7ExmW+DUAc0ywxtEbR3e0UND65rlSTVw==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-buffer-from": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/invalid-dependency": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.226.0.tgz", + "integrity": "sha512-QXOYFmap8g9QzRjumcRCIo2GEZkdCwd7ePQW0OABWPhKHzlJ74vvBxywjU3s39EEBEluWXtZ7Iufg6GxZM4ifw==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/is-array-buffer": { + "version": "3.201.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.201.0.tgz", + "integrity": "sha512-UPez5qLh3dNgt0DYnPD/q0mVJY84rA17QE26hVNOW3fAji8W2wrwrxdacWOxyXvlxWsVRcKmr+lay1MDqpAMfg==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-content-length": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.226.0.tgz", + "integrity": "sha512-ksUzlHJN2JMuyavjA46a4sctvnrnITqt2tbGGWWrAuXY1mel2j+VbgnmJUiwHKUO6bTFBBeft5Vd1TSOb4JmiA==", + "optional": true, + "dependencies": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.226.0.tgz", + "integrity": "sha512-EvLFafjtUxTT0AC9p3aBQu1/fjhWdIeK58jIXaNFONfZ3F8QbEYUPuF/SqZvJM6cWfOO9qwYKkRDbCSTYhprIg==", + "optional": true, + "dependencies": { + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/signature-v4": "3.226.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-config-provider": "3.208.0", + "@aws-sdk/util-middleware": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.226.0.tgz", + "integrity": "sha512-haVkWVh6BUPwKgWwkL6sDvTkcZWvJjv8AgC8jiQuSl8GLZdzHTB8Qhi3IsfFta9HAuoLjxheWBE5Z/L0UrfhLA==", + "optional": true, + "dependencies": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.226.0.tgz", + "integrity": "sha512-m9gtLrrYnpN6yckcQ09rV7ExWOLMuq8mMPF/K3DbL/YL0TuILu9i2T1W+JuxSX+K9FMG2HrLAKivE/kMLr55xA==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.226.0.tgz", + "integrity": "sha512-mwRbdKEUeuNH5TEkyZ5FWxp6bL2UC1WbY+LDv6YjHxmSMKpAoOueEdtU34PqDOLrpXXxIGHDFmjeGeMfktyEcA==", + "optional": true, + "dependencies": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-retry": { + "version": "3.235.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.235.0.tgz", + "integrity": "sha512-50WHbJGpD3SNp9763MAlHqIhXil++JdQbKejNpHg7HsJne/ao3ub+fDOfx//mMBjpzBV25BGd5UlfL6blrClSg==", + "optional": true, + "dependencies": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/service-error-classification": "3.229.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-middleware": "3.226.0", + "@aws-sdk/util-retry": "3.229.0", + "tslib": "^2.3.1", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sts": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.226.0.tgz", + "integrity": "sha512-NN9T/qoSD1kZvAT+VLny3NnlqgylYQcsgV3rvi/8lYzw/G/2s8VS6sm/VTWGGZhx08wZRv20MWzYu3bftcyqUg==", + "optional": true, + "dependencies": { + "@aws-sdk/middleware-signing": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/signature-v4": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-serde": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.226.0.tgz", + "integrity": "sha512-nPuOOAkSfx9TxzdKFx0X2bDlinOxGrqD7iof926K/AEflxGD1DBdcaDdjlYlPDW2CVE8LV/rAgbYuLxh/E/1VA==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.226.0.tgz", + "integrity": "sha512-E6HmtPcl+IjYDDzi1xI2HpCbBq2avNWcjvCriMZWuTAtRVpnA6XDDGW5GY85IfS3A8G8vuWqEVPr8JcYUcjfew==", + "optional": true, + "dependencies": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/signature-v4": "3.226.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-middleware": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-stack": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.226.0.tgz", + "integrity": "sha512-85wF29LvPvpoed60fZGDYLwv1Zpd/cM0C22WSSFPw1SSJeqO4gtFYyCg2squfT3KI6kF43IIkOCJ+L7GtryPug==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.226.0.tgz", + "integrity": "sha512-N1WnfzCW1Y5yWhVAphf8OPGTe8Df3vmV7/LdsoQfmpkCZgLZeK2o0xITkUQhRj1mbw7yp8tVFLFV3R2lMurdAQ==", + "optional": true, + "dependencies": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/node-config-provider": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.226.0.tgz", + "integrity": "sha512-B8lQDqiRk7X5izFEUMXmi8CZLOKCTWQJU9HQf3ako+sF0gexo4nHN3jhoRWyLtcgC5S3on/2jxpAcqtm7kuY3w==", + "optional": true, + "dependencies": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/node-http-handler": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.226.0.tgz", + "integrity": "sha512-xQCddnZNMiPmjr3W7HYM+f5ir4VfxgJh37eqZwX6EZmyItFpNNeVzKUgA920ka1VPz/ZUYB+2OFGiX3LCLkkaA==", + "optional": true, + "dependencies": { + "@aws-sdk/abort-controller": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/querystring-builder": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/property-provider": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.226.0.tgz", + "integrity": "sha512-TsljjG+Sg0LmdgfiAlWohluWKnxB/k8xenjeozZfzOr5bHmNHtdbWv6BtNvD/R83hw7SFXxbJHlD5H4u9p2NFg==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/protocol-http": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.226.0.tgz", + "integrity": "sha512-zWkVqiTA9RXL6y0hhfZc9bcU4DX2NI6Hw9IhQmSPeM59mdbPjJlY4bLlMr5YxywqO3yQ/ylNoAfrEzrDjlOSRg==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/querystring-builder": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.226.0.tgz", + "integrity": "sha512-LVurypuNeotO4lmirKXRC4NYrZRAyMJXuwO0f2a5ZAUJCjauwYrifKue6yCfU7bls7gut7nfcR6B99WBYpHs3g==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-uri-escape": "3.201.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/querystring-parser": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.226.0.tgz", + "integrity": "sha512-FzB+VrQ47KAFxiPt2YXrKZ8AOLZQqGTLCKHzx4bjxGmwgsjV8yIbtJiJhZLMcUQV4LtGeIY9ixIqQhGvnZHE4A==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/service-error-classification": { + "version": "3.229.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.229.0.tgz", + "integrity": "sha512-dnzWWQ0/NoWMUZ5C0DW3dPm0wC1O76Y/SpKbuJzWPkx1EYy6r8p32Ly4D9vUzrKDbRGf48YHIF2kOkBmu21CLg==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/shared-ini-file-loader": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.226.0.tgz", + "integrity": "sha512-661VQefsARxVyyV2FX9V61V+nNgImk7aN2hYlFKla6BCwZfMng+dEtD0xVGyg1PfRw0qvEv5LQyxMVgHcUSevA==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.226.0.tgz", + "integrity": "sha512-/R5q5agdPd7HJB68XMzpxrNPk158EHUvkFkuRu5Qf3kkkHebEzWEBlWoVpUe6ss4rP9Tqcue6xPuaftEmhjpYw==", + "optional": true, + "dependencies": { + "@aws-sdk/is-array-buffer": "3.201.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-hex-encoding": "3.201.0", + "@aws-sdk/util-middleware": "3.226.0", + "@aws-sdk/util-uri-escape": "3.201.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/smithy-client": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.234.0.tgz", + "integrity": "sha512-8AtR/k4vsFvjXeQbIzq/Wy7Nbk48Ou0wUEeVYPHWHPSU8QamFWORkOwmKtKMfHAyZvmqiAPeQqHFkq+UJhWyyQ==", + "optional": true, + "dependencies": { + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.241.0.tgz", + "integrity": "sha512-79okvuOS7V559OIL/RalIPG98wzmWxeFOChFnbEjn2pKOyGQ6FJRwLPYZaVRtNdAtnkBNgRpmFq9dX843QxhtQ==", + "optional": true, + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.241.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.226.0.tgz", + "integrity": "sha512-MmmNHrWeO4man7wpOwrAhXlevqtOV9ZLcH4RhnG5LmRce0RFOApx24HoKENfFCcOyCm5LQBlsXCqi0dZWDWU0A==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/url-parser": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.226.0.tgz", + "integrity": "sha512-p5RLE0QWyP0OcTOLmFcLdVgUcUEzmEfmdrnOxyNzomcYb0p3vUagA5zfa1HVK2azsQJFBv28GfvMnba9bGhObg==", + "optional": true, + "dependencies": { + "@aws-sdk/querystring-parser": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/util-base64": { + "version": "3.208.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.208.0.tgz", + "integrity": "sha512-PQniZph5A6N7uuEOQi+1hnMz/FSOK/8kMFyFO+4DgA1dZ5pcKcn5wiFwHkcTb/BsgVqQa3Jx0VHNnvhlS8JyTg==", + "optional": true, + "dependencies": { + "@aws-sdk/util-buffer-from": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-body-length-browser": { + "version": "3.188.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz", + "integrity": "sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/util-body-length-node": { + "version": "3.208.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.208.0.tgz", + "integrity": "sha512-3zj50e5g7t/MQf53SsuuSf0hEELzMtD8RX8C76f12OSRo2Bca4FLLYHe0TZbxcfQHom8/hOaeZEyTyMogMglqg==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-buffer-from": { + "version": "3.208.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.208.0.tgz", + "integrity": "sha512-7L0XUixNEFcLUGPeBF35enCvB9Xl+K6SQsmbrPk1P3mlV9mguWSDQqbOBwY1Ir0OVbD6H/ZOQU7hI/9RtRI0Zw==", + "optional": true, + "dependencies": { + "@aws-sdk/is-array-buffer": "3.201.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-config-provider": { + "version": "3.208.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.208.0.tgz", + "integrity": "sha512-DSRqwrERUsT34ug+anlMBIFooBEGwM8GejC7q00Y/9IPrQy50KnG5PW2NiTjuLKNi7pdEOlwTSEocJE15eDZIg==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-defaults-mode-browser": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.234.0.tgz", + "integrity": "sha512-IHMKXjTbOD8XMz5+2oCOsVP94BYb9YyjXdns0aAXr2NAo7k2+RCzXQ2DebJXppGda1F6opFutoKwyVSN0cmbMw==", + "optional": true, + "dependencies": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/util-defaults-mode-node": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.234.0.tgz", + "integrity": "sha512-UGjQ+OjBYYhxFVtUY+jtr0ZZgzZh6OHtYwRhFt8IHewJXFCfZTyfsbX20szBj5y1S4HRIUJ7cwBLIytTqMbI5w==", + "optional": true, + "dependencies": { + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.241.0.tgz", + "integrity": "sha512-jVf8bKrN22Ey0xLmj75sL7EUvm5HFpuOMkXsZkuXycKhCwLBcEUWlvtJYtRjOU1zScPQv9GMJd2QXQglp34iOQ==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-hex-encoding": { + "version": "3.201.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.201.0.tgz", + "integrity": "sha512-7t1vR1pVxKx0motd3X9rI3m/xNp78p3sHtP5yo4NP4ARpxyJ0fokBomY8ScaH2D/B+U5o9ARxldJUdMqyBlJcA==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.208.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.208.0.tgz", + "integrity": "sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-middleware": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.226.0.tgz", + "integrity": "sha512-B96CQnwX4gRvQdaQkdUtqvDPkrptV5+va6FVeJOocU/DbSYMAScLxtR3peMS8cnlOT6nL1Eoa42OI9AfZz1VwQ==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-retry": { + "version": "3.229.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.229.0.tgz", + "integrity": "sha512-0zKTqi0P1inD0LzIMuXRIYYQ/8c1lWMg/cfiqUcIAF1TpatlpZuN7umU0ierpBFud7S+zDgg0oemh+Nj8xliJw==", + "optional": true, + "dependencies": { + "@aws-sdk/service-error-classification": "3.229.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/util-uri-escape": { + "version": "3.201.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.201.0.tgz", + "integrity": "sha512-TeTWbGx4LU2c5rx0obHeDFeO9HvwYwQtMh1yniBz00pQb6Qt6YVOETVQikRZ+XRQwEyCg/dA375UplIpiy54mA==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.226.0.tgz", + "integrity": "sha512-PhBIu2h6sPJPcv2I7ELfFizdl5pNiL4LfxrasMCYXQkJvVnoXztHA1x+CQbXIdtZOIlpjC+6BjDcE0uhnpvfcA==", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.226.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.226.0.tgz", + "integrity": "sha512-othPc5Dz/pkYkxH+nZPhc1Al0HndQT8zHD4e9h+EZ+8lkd8n+IsnLfTS/mSJWrfiC6UlNRVw55cItstmJyMe/A==", + "optional": true, + "dependencies": { + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.188.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.188.0.tgz", + "integrity": "sha512-jt627x0+jE+Ydr9NwkFstg3cUvgWh56qdaqAMDsqgRlKD21md/6G226z/Qxl7lb1VEW2LlmCx43ai/37Qwcj2Q==", + "optional": true, + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/util-utf8-node": { + "version": "3.208.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.208.0.tgz", + "integrity": "sha512-jKY87Acv0yWBdFxx6bveagy5FYjz+dtV8IPT7ay1E2WPWH1czoIdMAkc8tSInK31T6CRnHWkLZ1qYwCbgRfERQ==", + "optional": true, + "dependencies": { + "@aws-sdk/util-buffer-from": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -58,6 +1159,46 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/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==" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -67,6 +1208,11 @@ "node": ">=8" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -90,6 +1236,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "optional": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -106,10 +1258,60 @@ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.0.tgz", + "integrity": "sha512-VrlEE4vuiO1WTpfof4VmaVolCVYkYTgB9iWgYNOrVlnifpME/06fhFRmONgBhClD5pFC1t9ZWqFUQEQAzY43bA==", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "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": ">=8" + "node": ">=10.16.0" } }, "node_modules/bytes": { @@ -159,6 +1361,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -192,11 +1402,43 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -230,6 +1472,14 @@ "node": ">=12" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -297,6 +1547,52 @@ "node": ">= 0.10.0" } }, + "node_modules/express-fileupload": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.4.0.tgz", + "integrity": "sha512-RjzLCHxkv3umDeZKeFeMg8w7qe0V09w3B7oGZprr/oO2H/ISCgNzuqzn7gV3HRWb37GjRk429CCpSLS2KNTqMQ==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express-mongo-sanitize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/express-mongo-sanitize/-/express-mongo-sanitize-2.2.0.tgz", + "integrity": "sha512-PZBs5nwhD6ek9ZuP+W2xmpvcrHwXZxD5GdieX2dsjPbAbH4azOkrHbycBud2QRU+YQF1CT+pki/lZGedHgo/dQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/express-rate-limit": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "engines": { + "node": ">= 12.9.0" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, + "node_modules/fast-xml-parser": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz", + "integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==", + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -417,6 +1713,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -443,6 +1759,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -454,6 +1789,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -504,6 +1844,83 @@ "node": ">=0.12.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.0.tgz", + "integrity": "sha512-rVBUGGwvqg130iwYu8k7lutHuDBFj1yGRdnlE44wEhxAmFBad1zcL66PdWC1raw3tIObY6XWhtv3VL04xQb/cg==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -512,6 +1929,12 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -567,6 +1990,124 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.12.1.tgz", + "integrity": "sha512-koT87tecZmxPKtxRQD8hCKfn+ockEL2xBiUvx3isQGI6mFmagWt4f4AyCE9J4sKepnLhMacoCTQQA6SLAI2L6w==", + "dependencies": { + "bson": "^4.7.0", + "mongodb-connection-string-url": "^2.5.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=12.9.0" + }, + "optionalDependencies": { + "@aws-sdk/credential-providers": "^3.186.0", + "saslprep": "^1.0.3" + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongoose": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.8.2.tgz", + "integrity": "sha512-cIato5N2w/QuJkkh0w4nyf7ty7DqmmP/W8/6PFSM0DrzbxIMlr6VN15LBIceTSJIxbznNl2Mlbh9Rm4sokMw+A==", + "dependencies": { + "bson": "^4.7.0", + "kareem": "2.5.0", + "mongodb": "4.12.1", + "mpath": "0.9.0", + "mquery": "4.0.3", + "ms": "2.1.3", + "sift": "16.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz", + "integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -580,6 +2121,64 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-geocoder": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/node-geocoder/-/node-geocoder-4.2.0.tgz", + "integrity": "sha512-ZvUiOHWHLVGlrYPvazXj+VQ9oK+EOMinh/5vWoBwOQiV0eCU7GPtEWrMRNnKvmBVOzZHb1eFe9uoKMFeyygMVA==", + "dependencies": { + "bluebird": "^3.5.2", + "node-fetch": "^2.6.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/nodemailer": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz", + "integrity": "sha512-jFaCEGTeT3E/m/5R2MHWiyQH3pSARECRUDM+1hokOYc3lQAAG7ASuy+2jIsYVf+RVa9zePopSQwKNVFH8DKUpA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", @@ -647,6 +2246,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -666,6 +2273,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -709,6 +2324,14 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -781,6 +2404,18 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -850,6 +2485,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + }, "node_modules/simple-update-notifier": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -871,6 +2511,45 @@ "semver": "bin/semver.js" } }, + "node_modules/slugify": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.5.tgz", + "integrity": "sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "optional": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -879,6 +2558,20 @@ "node": ">= 0.8" } }, + "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/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -923,6 +2616,23 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "optional": true + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -957,6 +2667,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -964,6 +2683,44 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/xss-clean": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/xss-clean/-/xss-clean-0.1.1.tgz", + "integrity": "sha512-On99yydxoAEkHpraR7Wjg9cD6UmKfbtH2HXO1it2djid32osHL7Qr8bIu+dGYoOeKzf3ZszLfz1gwR/D7whZ2A==", + "dependencies": { + "xss-filters": "1.2.6" + } + }, + "node_modules/xss-filters": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.6.tgz", + "integrity": "sha512-uqgwZRpVJCDfHsRX9lDrkPyCitQYzPklmLSbajJncATZKAUd1tF1x9y2VyPNFMv8SsSWed80xorSS5qGpw3WiA==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/package.json b/package.json index 46b6ec4..09ab4f7 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,24 @@ "author": "Albert Khomich", "license": "MIT", "dependencies": { + "bcryptjs": "^2.4.3", + "colors": "^1.4.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", "dotenv": "^16.0.3", - "express": "^4.18.2" + "express": "^4.18.2", + "express-fileupload": "^1.4.0", + "express-mongo-sanitize": "^2.2.0", + "express-rate-limit": "^6.7.0", + "helmet": "^6.0.1", + "hpp": "^0.2.3", + "jsonwebtoken": "^9.0.0", + "mongoose": "^6.8.2", + "morgan": "^1.10.0", + "node-geocoder": "^4.2.0", + "nodemailer": "^6.9.0", + "slugify": "^1.6.5", + "xss-clean": "^0.1.1" }, "devDependencies": { "nodemon": "^2.0.20" diff --git a/public/fonts/glyphicons-halflings-regular.eot b/public/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000..b93a495 Binary files /dev/null and b/public/fonts/glyphicons-halflings-regular.eot differ diff --git a/public/fonts/glyphicons-halflings-regular.woff b/public/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000..9e61285 Binary files /dev/null and b/public/fonts/glyphicons-halflings-regular.woff differ diff --git a/public/fonts/glyphicons-halflings-regular.woff2 b/public/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 0000000..64539b5 Binary files /dev/null and b/public/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..4fe03de --- /dev/null +++ b/public/index.html @@ -0,0 +1,195 @@ +DevCamper API + |  +Backend API for the DevCamper application to manage bootcamps, courses, reviews, users and authentications

DevCamper API

Backend API for the DevCamper application to manage bootcamps, courses, reviews, users and authentications

Bootcamps +7

Bootcamps CRUD functionality

Description

Fetch all bootcamps from database. Includes pagination, filtering, etc

Description

Add new bootcamp to database. Must be authenticated and must be publisher or admin

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"name": "ModernTech Bootcamp", +"description": "ModernTech has one goal, and that is to make you a rockstar developer and/or designer with a six figure salary. We teach both development and UI/UX", +"website": "https://moderntech.com", +"phone": "(222) 222-2222", +"email": "enroll@moderntech.com", +"address": "220 Pawtucket St, Lowell, MA 01854", +"careers": ["Web Development", "UI/UX", "Mobile Development"], +"housing": false, +"jobAssistance": true, +"jobGuarantee": false, +"acceptGi": true +}
Description

Update single bootcamp in database

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"careers": [ +"Web Development", +"UI/UX" +] +}
Description

Get bootcamps within a radius of a specific zipcode

Description

Rode to upload a bootcamp photo

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"careers": [ +"Web Development", +"UI/UX" +] +}

Courses +6

Create, read, update and delete courses

Description

Get all courses in Database

Description

Create a course for a specific bootcamp

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"title": "Full Stack Web Development", +"description": "In this course you will learn full stack web development, first learning all about the frontend with HTML/CSS/JS/Vue and then the backend with Node.js/Express/MongoDB", +"weeks": 12, +"tuition": 10000, +"minimumSkill": "intermediate", +"scholarhipsAvailable": true +}
Description

Update Course in DB

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"tuition": 13000, +"minimumSkill": "advanced" +}

Authentication +8

Routes for user authentication including register, login, reset password etc

Description

Add user to the database with encrypted password

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"name": "Albert Khomich", +"email": "albert@gmail.com", +"password": "123456", +"role": "publisher" +}
Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"email": "john@gmail.com", +"password": "123456" +}
Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Description

Generate password token and send email

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"email": "john@gmail.com" +}
Description

Reset user password using token

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"password": "012345" +}
Description

Update logged in user name and email

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"email": "john@gmail.com", +"name": "John Doe" +}
Description

Update logged in user password, send in the body currentPassword and newPassword

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"currentPassword": "123456", +"newPassword": "1234567" +}

Users +5

CRUD functionality for users only available to admins

Description

Add user to database (admin)

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"name": "Albert Khomich", +"email": "ak@gmail.com", +"password": "123456" +}
Description

Update user in database (admin)

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"name": "Albert Khomitch" +}

Reviews +6

Manage course reviews

Description

Get all reviews from database and populate with bootcamp name and description

Description

Fetch a review from database by id and populate Bootcamp name and description

Description

Insert review for a specific bootcamp

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"title": "Nice Bootcamp", +"text": "I learned a lot", +"rating": 8 +}
Description

Update review in database

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"title": "Had Fun" +}
Description

Remove review from database

Headers
KeyValueDescription
Content-Typeapplication/json

JSON Type

Body
{ +"title": "Had Fun" +}


\ No newline at end of file diff --git a/public/uploads/photo_5d725a1b7b292f5f8ceff788.jpg b/public/uploads/photo_5d725a1b7b292f5f8ceff788.jpg new file mode 100644 index 0000000..ec510d9 Binary files /dev/null and b/public/uploads/photo_5d725a1b7b292f5f8ceff788.jpg differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..10b0ddb --- /dev/null +++ b/readme.md @@ -0,0 +1,26 @@ +# DevCamper API + +> Backend API for DevCamper application, which is a bootcamp directory website + +## Usage + +Rename "config/config.env.env" to "config/config.env" and update the values/settings to your own + +## Install Dependencies + +``` +npm install +``` + +## Run App + +``` +# Run in dev mode +npm run dev + +# Run in prod mode +npm start +``` + +- Version 1.0.0 +- Licence MIT diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..edf847b --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,26 @@ +const express = require('express'); +const { + register, + login, + logout, + getMe, + forgotPassword, + resetPassword, + updateDetails, + updatePassword, +} = require('../controllers/auth'); + +const router = express.Router(); + +const { protect } = require('../middleware/auth'); + +router.post('/register', register); +router.post('/login', login); +router.get('/logout', logout); +router.get('/me', protect, getMe); +router.put('/updatedetails', protect, updateDetails); +router.put('/updatepassword', protect, updatePassword); +router.post('/forgotpassword', forgotPassword); +router.put('/resetpassword/:resettoken', resetPassword); + +module.exports = router; diff --git a/routes/bootcamps.js b/routes/bootcamps.js new file mode 100644 index 0000000..9b6ffff --- /dev/null +++ b/routes/bootcamps.js @@ -0,0 +1,44 @@ +const express = require('express'); +const { + getBootcamps, + getBootcamp, + createBootcamp, + updateBootcamp, + deleteBootcamp, + getBootcampsInRadius, + bootcampPhotoUpload, +} = require('../controllers/bootcamps'); + +const Bootcamp = require('../models/Bootcamp'); + +//Include other resource routers +const courseRouter = require('./courses'); +const reviewRouter = require('./reviews'); + +const router = express.Router(); + +const advancedResults = require('../middleware/advancedResults'); +const { protect, authorize } = require('../middleware/auth'); + +//Re-route into other resource routers +router.use('/:bootcampId/courses', courseRouter); +router.use('/:bootcampId/reviews', reviewRouter); + +router.route('/radius/:zipcode/:distance').get(getBootcampsInRadius); + +router + .route('/:id/photo') + .put(protect, authorize('publisher', 'admin'), bootcampPhotoUpload); + +router + .route('/') + .get(advancedResults(Bootcamp, 'courses'), getBootcamps) + .post(protect, authorize('publisher', 'admin'), createBootcamp); + +router + .route('/:id') + .get(getBootcamp) + .put(protect, authorize('publisher', 'admin'), updateBootcamp) + .delete(protect, authorize('publisher', 'admin'), deleteBootcamp); + +module.exports = router; diff --git a/routes/courses.js b/routes/courses.js new file mode 100644 index 0000000..b650b6d --- /dev/null +++ b/routes/courses.js @@ -0,0 +1,34 @@ +const express = require('express'); +const { + getCourses, + getCourse, + addCourse, + updateCourse, + deleteCourse, +} = require('../controllers/courses'); + +const Course = require('../models/Course'); + +const router = express.Router({ mergeParams: true }); + +const advancedResults = require('../middleware/advancedResults'); +const { protect, authorize } = require('../middleware/auth'); + +router + .route('/') + .get( + advancedResults(Course, { + path: 'bootcamp', + select: 'name description', + }), + getCourses + ) + .post(protect, authorize('publisher', 'admin'), addCourse); + +router + .route('/:id') + .get(getCourse) + .put(protect, authorize('publisher', 'admin'), updateCourse) + .delete(protect, authorize('publisher', 'admin'), deleteCourse); + +module.exports = router; diff --git a/routes/reviews.js b/routes/reviews.js new file mode 100644 index 0000000..1a2a67b --- /dev/null +++ b/routes/reviews.js @@ -0,0 +1,34 @@ +const express = require('express'); +const { + getReviews, + getReview, + addReview, + updateReview, + deleteReview, +} = require('../controllers/reviews'); + +const Review = require('../models/Review'); + +const router = express.Router({ mergeParams: true }); + +const advancedResults = require('../middleware/advancedResults'); +const { protect, authorize } = require('../middleware/auth'); + +router + .route('/') + .get( + advancedResults(Review, { + path: 'bootcamp', + select: 'name description', + }), + getReviews + ) + .post(protect, authorize('user', 'admin'), addReview); + +router + .route('/:id') + .get(getReview) + .put(protect, authorize('user', 'admin'), updateReview) + .delete(protect, authorize('user', 'admin'), deleteReview); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..886f660 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,24 @@ +const express = require('express'); +const { + getUsers, + getUser, + createUser, + updateUser, + deleteUser, +} = require('../controllers/users'); + +const User = require('../models/User'); + +const router = express.Router({ mergeParams: true }); + +const advancedResults = require('../middleware/advancedResults'); +const { protect, authorize } = require('../middleware/auth'); + +router.use(protect); +router.use(authorize('admin')); + +router.route('/').get(advancedResults(User), getUsers).post(createUser); + +router.route('/:id').get(getUser).put(updateUser).delete(deleteUser); + +module.exports = router; diff --git a/seeder.js b/seeder.js new file mode 100644 index 0000000..d2e08d9 --- /dev/null +++ b/seeder.js @@ -0,0 +1,69 @@ +const fs = require('fs'); +const mongoose = require('mongoose'); +const colors = require('colors'); +const dotenv = require('dotenv'); + +// Load env vars +dotenv.config({ path: './config/config.env' }); + +//Load models +const Bootcamp = require('./models/Bootcamp'); +const Course = require('./models/Course'); +const User = require('./models/User'); +const Review = require('./models/Review'); + +//Connect to DB +mongoose.connect(process.env.MONGO_URI, { + useNewUrlParser: true, +}); + +// Read JSON files +const bootcamps = JSON.parse( + fs.readFileSync(`${__dirname}/_data/bootcamps.json`, 'utf8') +); + +const courses = JSON.parse( + fs.readFileSync(`${__dirname}/_data/courses.json`, 'utf8') +); + +const users = JSON.parse( + fs.readFileSync(`${__dirname}/_data/users.json`, 'utf8') +); + +const reviews = JSON.parse( + fs.readFileSync(`${__dirname}/_data/reviews.json`, 'utf8') +); + +// Import into DB +const importData = async () => { + try { + await Bootcamp.create(bootcamps); + await Course.create(courses); + await Review.create(reviews); + await User.create(users); + console.log('Data imported...'.green.inverse); + process.exit(); + } catch (err) { + console.error(err); + } +}; + +//Delete data +const deleteData = async () => { + try { + await Bootcamp.deleteMany(); + await Course.deleteMany(); + await User.deleteMany(); + await Review.deleteMany(); + console.log('Data destroyed...'.red.inverse); + process.exit(); + } catch (err) { + console.error(err); + } +}; + +if (process.argv[2] === '-i') { + importData(); +} else if (process.argv[2] === '-d') { + deleteData(); +} diff --git a/server.js b/server.js index 0aa4a2a..22b5f4b 100644 --- a/server.js +++ b/server.js @@ -1,14 +1,94 @@ +const path = require('path'); const express = require('express'); const dotenv = require('dotenv'); +const morgan = require('morgan'); +const colors = require('colors'); +const fileupload = require('express-fileupload'); +const cookieParser = require('cookie-parser'); +const mongoSanitize = require('express-mongo-sanitize'); +const helmet = require('helmet'); +const xss = require('xss-clean'); +const rateLimit = require('express-rate-limit'); +const hpp = require('hpp'); +const cors = require('cors'); +const errorHandler = require('./middleware/error'); +const connectDB = require('./config/db'); //Load env vars dotenv.config({ path: './config/config.env' }); +//Connect to database +connectDB(); + +//Route files +const bootcamps = require('./routes/bootcamps'); +const courses = require('./routes/courses'); +const auth = require('./routes/auth'); +const users = require('./routes/users'); +const reviews = require('./routes/reviews'); + const app = express(); +//Body parser +app.use(express.json()); + +//Cookie parser +app.use(cookieParser()); + +// Dev logging middleware +if (process.env.NODE_ENV === 'development') { + app.use(morgan('dev')); +} + +//File uploading +app.use(fileupload()); + +// Sanitize data +app.use(mongoSanitize()); + +// Set security headers +app.use(helmet()); + +// Prevent XSS attacks +app.use(xss()); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 10 * 60 * 1000, + max: 100, +}); +app.use(limiter); + +// Prevent http param pollution +app.use(hpp()); + +// Enable CORS +app.use(cors()); + +//Set static folder +app.use(express.static(path.join(__dirname, 'public'))); + +//Mount routers +app.use('/api/v1/bootcamps', bootcamps); +app.use('/api/v1/courses', courses); +app.use('/api/v1/auth', auth); +app.use('/api/v1/users', users); +app.use('/api/v1/reviews', reviews); + +app.use(errorHandler); + const PORT = process.env.PORT || 5000; -app.listen( - PORT, - console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`) -); \ No newline at end of file +const server = app.listen( + PORT, + console.log( + `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow.bold + ) +); + +//Handle unhandled promise rejections +process.on('unhandledRejection', (err, promise) => { + console.log(`Error: ${err.message}`.red); + //Close server & exit process + server.close(() => process.exit(1)); +}); diff --git a/utils/errorResponse.js b/utils/errorResponse.js new file mode 100644 index 0000000..29d4ac9 --- /dev/null +++ b/utils/errorResponse.js @@ -0,0 +1,8 @@ +class ErrorResponse extends Error { + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + } +} + +module.exports = ErrorResponse; diff --git a/utils/geocoder.js b/utils/geocoder.js new file mode 100644 index 0000000..223b636 --- /dev/null +++ b/utils/geocoder.js @@ -0,0 +1,12 @@ +const NodeGeocoder = require('node-geocoder'); + +const options = { + provider: process.env.GEOCODER_PROVIDER, + httpAdapter: 'https', + apiKey: process.env.GEOCODER_API_KEY, + formatter: null, +}; + +const geocoder = NodeGeocoder(options); + +module.exports = geocoder; diff --git a/utils/sendEmail.js b/utils/sendEmail.js new file mode 100644 index 0000000..4ae0e2d --- /dev/null +++ b/utils/sendEmail.js @@ -0,0 +1,25 @@ +const nodemailer = require('nodemailer'); + +const sendEmail = async (options) => { + const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + auth: { + user: process.env.SMTP_EMAIL, + pass: process.env.SMTP_PASSWORD, + }, + }); + + const message = { + from: `${process.env.FROM_NAME} <${process.env.FROM_EMAIL}>`, + to: options.email, + subject: options.subject, + text: options.message, + }; + + const info = await transporter.sendMail(message); + + console.log('Message sent: %s', info.messageId); +}; + +module.exports = sendEmail;