├── .gitignore ├── README.md ├── backend ├── config │ └── db.js ├── controllers │ ├── noteController.js │ ├── ticketController.js │ └── userController.js ├── middleware │ ├── authMiddleware.js │ └── errorMiddleware.js ├── models │ ├── noteModel.js │ ├── ticketModel.js │ └── userModel.js ├── routes │ ├── noteRoutes.js │ ├── ticketRoutes.js │ └── userRoutes.js └── server.js ├── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── app │ └── store.js │ ├── components │ ├── BackButton.jsx │ ├── Header.jsx │ ├── NoteItem.jsx │ ├── PrivateRoute.jsx │ ├── Spinner.jsx │ └── TicketItem.jsx │ ├── features │ ├── auth │ │ ├── authService.js │ │ └── authSlice.js │ ├── notes │ │ ├── noteService.js │ │ └── noteSlice.js │ └── tickets │ │ ├── ticketService.js │ │ └── ticketSlice.js │ ├── hooks │ └── useAuthStatus.js │ ├── index.css │ ├── index.js │ ├── pages │ ├── Home.jsx │ ├── Login.jsx │ ├── NewTicket.jsx │ ├── Register.jsx │ ├── Ticket.jsx │ └── Tickets.jsx │ └── serviceWorker.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | mern-app-generator 4 | 5 |

6 |

Create, build and deploy MERN stack application.

7 | 8 | ## Introduction 9 | A Fullstack MERN Support Ticket System with Redux Toolkit. 10 | ![MERN Stack](https://miro.medium.com/max/1400/1*u8xh3we2xdp9piDGFpaHSg.png) 11 | ![MERN Architerure](https://www.bocasay.com/wp-content/uploads/2020/03/MERN-stack-1.png) 12 | 13 | ## Getting Started 14 | 15 | 16 | ### Built With 17 | 18 | This section should list any major frameworks/libraries used to bootstrap your project. 19 | 20 | * [MongoDB Atlas](https://www.mongodb.com/atlas/database) 21 | **Database**. Deploy a multi-cloud database. 22 | The most advanced cloud database service on the market, with unmatched data distribution and mobility across AWS, Azure, and Google Cloud, built-in automation for resource and workload optimization, and so much more. 23 | * [MongoDB Compass](https://www.mongodb.com/products/compass) 24 | **Compass**. The GUI for MongoDB. 25 | Compass is an interactive tool for querying, optimizing, and analyzing your MongoDB data. Get key insights, drag and drop to build pipelines, and more. 26 | * [Express.js](https://expressjs.com/) 27 | Express.js, or simply Express, is a back end web application framework for Node.js, released as free and open-source software under the MIT License. It is designed for building web applications and APIs. It has been called the de facto standard server framework for Node.js. 28 | * [React.js](https://reactjs.org/) 29 | A JavaScript library for building user interfaces. 30 | * [Node.js](https://nodejs.org/en/) 31 | Node.js is a free, open-sourced, cross-platform JavaScript run-time environment that lets developers write command line tools and server-side scripts outside of a browser. 32 | * [Redux Toolkit](https://redux-toolkit.js.org/) 33 | The official, opinionated, batteries-included toolset for efficient Redux development. 34 | * [Postman](https://www.postman.com/) 35 | Postman is an application used for API testing. 36 | 37 | 38 | ### Deployed On 39 | * [HEROKU](https://heroku.com/) 40 | 41 | [Support Ticket App](https://support-desk-mern-arifmd.herokuapp.com/) 42 | 43 | ### Installation 44 | 45 | ### .env 46 | 47 | Lets create a `.env` file in the root of the project: 48 | 49 | ```bash 50 | touch .env 51 | ``` 52 | 53 | Then put the following code in that `.env` except you should add your details. 54 | 55 | ```bash 56 | NODE_ENV = development 57 | PORT = 5000 58 | MONGODB_URL= 59 | JWT_SECRET= 60 | ``` 61 | 62 | Provided in the root of the project, a `.sample.env` for guidance. 63 | 64 | 65 | ### Folder Structure 66 | 67 | ``` 68 | ├── README.md 69 | ├── backend 70 | │   ├── config 71 | │   │   └── db.js 72 | │   ├── controllers 73 | │   │   ├── noteController.js 74 | │   │   ├── ticketController.js 75 | │   │   └── userController.js 76 | │   ├── middleware 77 | │   │   ├── authMiddleware.js 78 | │   │   └── errorMiddleware.js 79 | │   ├── models 80 | │   │   ├── noteModel.js 81 | │   │   ├── ticketModel.js 82 | │   │   └── userModel.js 83 | │   ├── routes 84 | │   │   ├── noteRoutes.js 85 | │   │   ├── ticketRoutes.js 86 | │   │   └── userRoutes.js 87 | │   └── server.js 88 | ├── frontend 89 | │   ├── README.md 90 | │   ├── package-lock.json 91 | │   ├── package.json 92 | │   ├── public 93 | │   │   ├── favicon.ico 94 | │   │   ├── index.html 95 | │   │   ├── logo192.png 96 | │   │   ├── logo512.png 97 | │   │   ├── manifest.json 98 | │   │   └── robots.txt 99 | │   └── src 100 | │   ├── App.js 101 | │   ├── app 102 | │   │   └── store.js 103 | │   ├── components 104 | │   │   ├── BackButton.jsx 105 | │   │   ├── Header.jsx 106 | │   │   ├── NoteItem.jsx 107 | │   │   ├── PrivateRoute.jsx 108 | │   │   ├── Spinner.jsx 109 | │   │   └── TicketItem.jsx 110 | │   ├── features 111 | │   │   ├── auth 112 | │   │   │   ├── authService.js 113 | │   │   │   └── authSlice.js 114 | │   │   ├── notes 115 | │   │   │   ├── noteService.js 116 | │   │   │   └── noteSlice.js 117 | │   │   └── tickets 118 | │   │   ├── ticketService.js 119 | │   │   └── ticketSlice.js 120 | │   ├── hooks 121 | │   │   └── useAuthStatus.js 122 | │   ├── index.css 123 | │   ├── index.js 124 | │   ├── pages 125 | │   │   ├── Home.jsx 126 | │   │   ├── Login.jsx 127 | │   │   ├── NewTicket.jsx 128 | │   │   ├── Register.jsx 129 | │   │   ├── Ticket.jsx 130 | │   │   └── Tickets.jsx 131 | │   └── serviceWorker.js 132 | ├── node_modules 133 | ├── package-lock.json 134 | └── package.json 135 | ``` 136 | 137 | ### Run backend & frontend servers concurrently 138 | 139 | In order to run the backend & frontend servers using [concurrently](https://www.npmjs.com/package/concurrently): 140 | 141 | ```sh 142 | npm run dev 143 | ``` 144 | 145 | This will automatically open the local development server at [http://localhost:3000](http://localhost:3000). 146 | 147 | The page will automatically reload if you make changes to the code.
148 | You will see the build errors and lint warnings in the console. 149 | 150 | ### Routes 151 | 152 | Inside of `/backend/controllers/userController.js` are a collection of routes that involve `CRUD` functions of persistent storage. 153 | 154 | - **POST** `/api/users` - Register/create a new user using **registerUser** method in User Controller, requires a **URL-encoded** data in the **Body** containing key value of { name, email, password }, it returns the unique ID along with JWT token 155 | - **POST** `/api/users/login` - Register/create a new user using **loginUser** method in User Controller, requires a **URL-encoded** data in the **Body** containing key value of { email, password }, it returns the registered numeric ID from DB along with unique generated JWT token 156 | - **GET** `/api/users/me` - Get current user 157 | 158 | Inside of `/backend/controllers/ticketController.js` are a collection of. 159 | 160 | getTickets, [done] 161 | getTicket, [done] 162 | createTicket, [done] 163 | updateTicket 164 | deleteTicket, 165 | 166 | - **GET** `/api/tickets` - Get all the tickets of logged in user while passing the right `Authorization` `Bearer Token`. 167 | - **POST** `/api/tickets` - Create a new ticket, requires a **URL-encoded** data in the **Body** containing key value of { product, description } 168 | - **GET** `/api/tickets/:id` - Get the particular ticket detail while passing the right `Authorization` `Bearer Token`. 169 | - 170 | [Postman](https://learning.postman.com/docs/sending-requests/requests/#sending-body-data) 171 | 172 | ### Build an application 173 | 174 | In order to make a production build of your application: 175 | 176 | ```sh 177 | npm run build 178 | ``` 179 | 180 | This will produce an optimized build of your application in `build` folder. 181 | 182 | ### Deploy your application 183 | 184 | In order to produce a ready-to-deploy version of your application to deploy to Heroku: 185 | 186 | ```sh 187 | npm run deploy 188 | ``` 189 | 190 | This will produce a ready-to-deploy version of your application in `deploy` folder. 191 | Now you can deploy your application by running few handful commands: 192 | 193 | ```sh 194 | cd deploy 195 | heroku login 196 | git init 197 | git add * 198 | git commit -m "deploying my-app" 199 | heroku create my-app 200 | git push heroku master 201 | ``` 202 | And within a few seconds, your application will be live at [https://support-desk-mern-arifmd.herokuapp.com/](https://support-desk-mern-arifmd.herokuapp.com/). 203 | 204 | 205 | ## Author(s) 206 | 207 | - **Mohammad Arif** - [mdarif](https://github.com/mdarif) 208 | -------------------------------------------------------------------------------- /backend/config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const connectDB = async () => { 4 | try { 5 | const conn = await mongoose.connect(process.env.MONGO_URI) 6 | console.log( 7 | `MongoDB Connected: ${conn.connection.host}`.cyan.underline.bold 8 | ) 9 | } catch (error) { 10 | console.log(`Error: ${error.message}`.red.underline.bold) 11 | process.exit(1) // exit process with failure 12 | } 13 | } 14 | 15 | module.exports = connectDB 16 | -------------------------------------------------------------------------------- /backend/controllers/noteController.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = require('express-async-handler') 2 | 3 | const User = require('../models/userModel') 4 | const Note = require('../models/noteModel') 5 | const Ticket = require('../models/ticketModel') 6 | 7 | // @desc Get notes for a ticket 8 | // @route GET /api/tickets/:ticketId/notes 9 | // @access Private 10 | 11 | /** 12 | * 'asyncHandler' is a simple middleware for handling exceptions 13 | * inside of async express routes and passing them to your express 14 | * error handlers. 15 | */ 16 | const getNotes = asyncHandler(async (req, res) => { 17 | // Get user using the id and JWT 18 | const user = await User.findById(req.user.id) 19 | 20 | if (!user) { 21 | res.status(401) 22 | throw new Error('User not found') 23 | } 24 | 25 | const ticket = await Ticket.findById(req.params.ticketId) 26 | 27 | if (ticket.user.toString() !== req.user.id) { 28 | res.status(401) 29 | throw new Error('User not authorized') 30 | } 31 | 32 | const notes = await Note.find({ ticket: req.params.ticketId }) 33 | 34 | res.status(200).json(notes) 35 | }) 36 | 37 | // @desc Create ticket note 38 | // @route POST /api/tickets/:ticketId/notes 39 | // @access Private 40 | 41 | const addNote = asyncHandler(async (req, res) => { 42 | // Get user using the id and JWT 43 | const user = await User.findById(req.user.id) 44 | 45 | if (!user) { 46 | res.status(401) 47 | throw new Error('User not found') 48 | } 49 | 50 | const ticket = await Ticket.findById(req.params.ticketId) 51 | 52 | if (ticket.user.toString() !== req.user.id) { 53 | res.status(401) 54 | throw new Error('User not authorized') 55 | } 56 | 57 | const note = await Note.create({ 58 | ticket: req.params.ticketId, 59 | text: req.body.text, 60 | isStaff: false, 61 | user: req.user.id 62 | }) 63 | 64 | res.status(200).json(note) 65 | }) 66 | 67 | module.exports = { 68 | getNotes, 69 | addNote 70 | } 71 | -------------------------------------------------------------------------------- /backend/controllers/ticketController.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = require('express-async-handler') 2 | 3 | const User = require('../models/userModel') 4 | const Ticket = require('../models/ticketModel') 5 | 6 | // @desc Get user tickets 7 | // @route GET /api/tickets 8 | // @access Private 9 | 10 | /** 11 | * 'asyncHandler' is a simple middleware for handling exceptions 12 | * inside of async express routes and passing them to your express 13 | * error handlers. 14 | */ 15 | const getTickets = asyncHandler(async (req, res) => { 16 | // Get user using the id and JWT 17 | const user = await User.findById(req.user.id) 18 | 19 | if (!user) { 20 | res.status(401) 21 | throw new Error('User not found') 22 | } 23 | 24 | const tickets = await Ticket.find({ user: req.user._id }) 25 | 26 | res.status(200).json(tickets) 27 | }) 28 | 29 | // @desc Get user ticket 30 | // @route GET /api/tickets/:id 31 | // @access Private 32 | const getTicket = asyncHandler(async (req, res) => { 33 | // Get user using the id and JWT 34 | const user = await User.findById(req.user.id) 35 | 36 | if (!user) { 37 | res.status(401) 38 | throw new Error('User not found') 39 | } 40 | 41 | const ticket = await Ticket.findById(req.params.id) 42 | 43 | if (!ticket) { 44 | res.status(404) 45 | throw new Error('Ticket not found') 46 | } 47 | 48 | // Check if ticket belongs to user 49 | if (ticket.user.toString() !== req.user.id) { 50 | res.status(401) 51 | throw new Error('Not authorized') 52 | } 53 | 54 | res.status(200).json(ticket) 55 | }) 56 | 57 | // @desc Create new ticket 58 | // @route POST /api/ticket 59 | // @access Private 60 | const createTicket = asyncHandler(async (req, res) => { 61 | const { product, description } = req.body 62 | 63 | if (!product || !description) { 64 | res.status(400) // Bad request 65 | throw new Error('Please provide a product and description') 66 | } 67 | 68 | // Get user using the id and JWT 69 | const user = await User.findById(req.user.id) 70 | 71 | if (!user) { 72 | res.status(401) 73 | throw new Error('User not found') 74 | } 75 | 76 | const ticket = await Ticket.create({ 77 | product, 78 | description, 79 | user: req.user.id, 80 | status: 'new' 81 | }) 82 | 83 | res.status(201).json(ticket) 84 | }) 85 | 86 | // @desc Delete ticket 87 | // @route DELETE /api/tickets/:id 88 | // @access Private 89 | const deleteTicket = asyncHandler(async (req, res) => { 90 | // Get user using the id and JWT 91 | const user = await User.findById(req.user.id) 92 | 93 | if (!user) { 94 | res.status(401) 95 | throw new Error('User not found') 96 | } 97 | 98 | const ticket = await Ticket.findById(req.params.id) 99 | 100 | if (!ticket) { 101 | res.status(404) 102 | throw new Error('Ticket not found') 103 | } 104 | 105 | // Check if ticket belongs to user 106 | if (ticket.user.toString() !== req.user.id) { 107 | res.status(401) 108 | throw new Error('Not authorized') 109 | } 110 | 111 | await ticket.remove() 112 | 113 | res.status(200).json({ success: true }) 114 | }) 115 | 116 | // @desc Update ticket 117 | // @route PUT /api/tickets/:id 118 | // @access Private 119 | const updateTicket = asyncHandler(async (req, res) => { 120 | // Get user using the id and JWT 121 | const user = await User.findById(req.user.id) 122 | 123 | if (!user) { 124 | res.status(401) 125 | throw new Error('User not found') 126 | } 127 | 128 | const ticket = await Ticket.findById(req.params.id) 129 | 130 | if (!ticket) { 131 | res.status(404) 132 | throw new Error('Ticket not found') 133 | } 134 | 135 | // Check if ticket belongs to user 136 | if (ticket.user.toString() !== req.user.id) { 137 | res.status(401) 138 | throw new Error('Not authorized') 139 | } 140 | 141 | const updatedTicket = await Ticket.findByIdAndUpdate( 142 | req.params.id, 143 | req.body, 144 | { 145 | new: true 146 | } 147 | ) 148 | 149 | res.status(200).json(updatedTicket) 150 | }) 151 | 152 | module.exports = { 153 | getTickets, 154 | createTicket, 155 | getTicket, 156 | deleteTicket, 157 | updateTicket 158 | } 159 | -------------------------------------------------------------------------------- /backend/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = require('express-async-handler') // Simple middleware for handling exceptions inside of async express routes and passing them to your express error handlers. 2 | const jwt = require('jsonwebtoken') // JSON Web Token for authentication and authorization 3 | const bcrypt = require('bcryptjs') // A library to help you hash passwords. 4 | 5 | const User = require('../models/userModel') 6 | 7 | // @desc Register a new user 8 | // @route /api/users 9 | // @access Public 10 | 11 | /** 12 | * 'asyncHandler' is a simple middleware for handling exceptions 13 | * inside of async express routes and passing them to your express 14 | * error handlers. 15 | */ 16 | const registerUser = asyncHandler(async (req, res) => { 17 | const { name, email, password } = req.body // destructure the request body params 18 | 19 | // Validation 20 | if (!name || !email || !password) { 21 | res.status(400) 22 | throw new Error('Please provide all required fields') 23 | } 24 | 25 | // Check for existing user 26 | const userExists = await User.findOne({ email }) 27 | 28 | if (userExists) { 29 | res.status(400) 30 | throw new Error('User already exists') 31 | } 32 | 33 | // Hash password 34 | const salt = await bcrypt.genSalt(10) // 10 is the number of rounds 35 | const hashedPassword = await bcrypt.hash(password, salt) // hash the password 36 | 37 | // Create user 38 | const user = await User.create({ 39 | name, 40 | email, 41 | password: hashedPassword 42 | }) 43 | 44 | // User is created 45 | if (user) { 46 | res.status(201).json({ 47 | _id: user._id, 48 | name: user.name, 49 | email: user.email, 50 | token: generateToken(user._id) 51 | }) 52 | } else { 53 | res.status(400) 54 | throw new Error('User could not be created') 55 | } 56 | }) 57 | 58 | // @desc Login a user 59 | // @route /api/users/login 60 | // @access Public 61 | const loginUser = asyncHandler(async (req, res) => { 62 | const { email, password } = req.body // destructuring 63 | 64 | const user = await User.findOne({ email }) 65 | 66 | // Check User and Password match 67 | if (user && (await bcrypt.compare(password, user.password))) { 68 | res.status(200).json({ 69 | _id: user._id, 70 | name: user.name, 71 | email: user.email, 72 | token: generateToken(user._id) 73 | }) 74 | } else { 75 | res.status(401) // Unauthorized 76 | throw new Error('Invalid credentials') 77 | } 78 | }) 79 | 80 | // @desc Get current user 81 | // @route /api/users/me 82 | // @access Private 83 | const getMe = asyncHandler(async (req, res) => { 84 | const user = { 85 | id: req.user._id, 86 | email: req.user.email, 87 | name: req.user.name 88 | } 89 | res.status(200).json(user) 90 | }) 91 | 92 | // Generate token 93 | generateToken = id => { 94 | return jwt.sign({ id }, process.env.JWT_SECRET, { 95 | expiresIn: '30d' 96 | }) 97 | } 98 | 99 | module.exports = { 100 | registerUser, 101 | loginUser, 102 | getMe 103 | } 104 | -------------------------------------------------------------------------------- /backend/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const asyncHandler = require('express-async-handler') 3 | const User = require('../models/userModel') 4 | 5 | const protect = asyncHandler(async (req, res, next) => { 6 | let token 7 | 8 | // console.log(req.headers.authorization) 9 | // Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYyMWM3NTc4MzBjODJmYjc2ZWY0ZDMxNyIsImlhdCI6MTY0NjAzMjM2MSwiZXhwIjoxNjQ4NjI0MzYxfQ.ISQuHgbha-Q4kvn8GDUEwTONK0gn_QH86E6SQKQTg78 10 | if ( 11 | req.headers.authorization && 12 | req.headers.authorization.startsWith('Bearer') 13 | ) { 14 | try { 15 | // Get token from header 16 | token = req.headers.authorization.split(' ')[1] 17 | // Verify token 18 | const decoded = jwt.verify(token, process.env.JWT_SECRET) 19 | // Get user from token 20 | req.user = await User.findById(decoded.id).select('-password') 21 | // console.log(`token: ${token} decoded: ${decoded} user: ${req.user}`) 22 | 23 | next() // Continue to next middleware 24 | } catch (error) { 25 | console.log(error) 26 | res.status(401) 27 | throw new Error('Not authorized to access this route') 28 | } 29 | } 30 | 31 | if (!token) { 32 | res.status(401) 33 | throw new Error('Not authorized to access this route') 34 | } 35 | }) 36 | 37 | module.exports = { protect } 38 | -------------------------------------------------------------------------------- /backend/middleware/errorMiddleware.js: -------------------------------------------------------------------------------- 1 | const errorHandler = (err, req, res, next) => { 2 | const statusCode = res.statusCode ? res.statusCode : 500 3 | res.status(statusCode) 4 | res.json({ 5 | message: err.message, 6 | stack: process.env.NODE_ENV === 'production' ? null : err.stack 7 | }) 8 | } 9 | 10 | module.exports = { errorHandler } 11 | -------------------------------------------------------------------------------- /backend/models/noteModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const noteSchema = mongoose.Schema( 4 | { 5 | user: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | required: true, 8 | ref: 'User' 9 | }, 10 | ticket: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | required: true, 13 | ref: 'Ticket' 14 | }, 15 | text: { 16 | type: String, 17 | required: [true, 'Please add some text'] 18 | }, 19 | isStaff: { 20 | type: Boolean, 21 | default: false 22 | }, 23 | staffId: { 24 | type: String 25 | } 26 | }, 27 | { 28 | timestamps: true 29 | } 30 | ) 31 | 32 | module.exports = mongoose.model('Note', noteSchema) 33 | -------------------------------------------------------------------------------- /backend/models/ticketModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const ticketSchema = mongoose.Schema( 4 | { 5 | user: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | required: true, 8 | ref: 'User' 9 | }, 10 | product: { 11 | type: String, 12 | required: [true, 'Please select a product'], 13 | enum: [ 14 | 'iPhone', 15 | 'iPad', 16 | 'MacBook', 17 | 'Macbook Pro', 18 | 'iMac', 19 | 'iPod', 20 | 'iPod touch' 21 | ] 22 | }, 23 | description: { 24 | type: String, 25 | required: [true, 'Please enter a decription of the issue'] 26 | }, 27 | status: { 28 | type: String, 29 | required: true, 30 | enum: ['new', 'open', 'close'], 31 | default: 'new' 32 | } 33 | }, 34 | { 35 | timestamps: true 36 | } 37 | ) 38 | 39 | module.exports = mongoose.model('Ticket', ticketSchema) 40 | -------------------------------------------------------------------------------- /backend/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const userSchema = mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: [true, 'Please add a name'] 8 | }, 9 | email: { 10 | type: String, 11 | required: [true, 'Please add a email'], 12 | unique: true 13 | }, 14 | password: { 15 | type: String, 16 | required: [true, 'Please add a password'] 17 | }, 18 | isAdmin: { 19 | type: Boolean, 20 | required: true, 21 | default: false 22 | } 23 | }, 24 | { 25 | timestamps: true 26 | } 27 | ) 28 | 29 | module.exports = mongoose.model('User', userSchema) 30 | -------------------------------------------------------------------------------- /backend/routes/noteRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router({ mergeParams: true }) 3 | const { getNotes, addNote } = require('../controllers/noteController') 4 | 5 | const { protect } = require('../middleware/authMiddleware') 6 | 7 | router 8 | .route('/') 9 | .get(protect, getNotes) 10 | .post(protect, addNote) 11 | 12 | module.exports = router 13 | 14 | // /api/tickets/:ticketId/notes 15 | -------------------------------------------------------------------------------- /backend/routes/ticketRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const { 4 | getTickets, 5 | getTicket, 6 | createTicket, 7 | updateTicket, 8 | deleteTicket 9 | } = require('../controllers/ticketController') 10 | 11 | const { protect } = require('../middleware/authMiddleware') 12 | 13 | // Re-route into note router 14 | const noteRouter = require('./noteRoutes') 15 | router.use('/:ticketId/notes', noteRouter) 16 | 17 | // Protected route to create the ticket 18 | router 19 | .route('/') 20 | .get(protect, getTickets) 21 | .post(protect, createTicket) 22 | 23 | router 24 | .route('/:id') 25 | .get(protect, getTicket) 26 | .delete(protect, deleteTicket) 27 | .put(protect, updateTicket) 28 | 29 | module.exports = router 30 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const { 4 | registerUser, 5 | loginUser, 6 | getMe 7 | } = require('../controllers/userController') 8 | 9 | const { protect } = require('../middleware/authMiddleware') 10 | 11 | router.post('/', registerUser) 12 | 13 | router.post('/login', loginUser) 14 | 15 | // Protected route (2nd argument) - protect 16 | router.get('/me', protect, getMe) 17 | 18 | module.exports = router 19 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') // commonjs module syntax 2 | const colors = require('colors') 3 | const dotenv = require('dotenv').config() 4 | const { errorHandler } = require('./middleware/errorMiddleware') 5 | const connectDB = require('./config/db') 6 | const path = require('path') 7 | const PORT = process.env.PORT || 3000 8 | 9 | // Connect to database 10 | connectDB() 11 | 12 | const app = express() 13 | 14 | /** 15 | * Each app.use(middleware) is called every time 16 | * a request is sent to the server 17 | */ 18 | 19 | /** 20 | * This is a built-in middleware function in Express. 21 | * It parses incoming requests with JSON payloads and is based on body-parser. 22 | */ 23 | app.use(express.json()) 24 | 25 | /** 26 | * This is a built-in middleware function in Express. 27 | * It parses incoming requests (Object as strings or arrays) with 28 | * urlencoded payloads and is based on body-parser. 29 | */ 30 | app.use(express.urlencoded({ extended: false })) 31 | 32 | // Routes endpoints 33 | app.use('/api/users', require('./routes/userRoutes')) 34 | app.use('/api/tickets', require('./routes/ticketRoutes')) 35 | 36 | // Serve Frontend 37 | if (process.env.NODE_ENV === 'production') { 38 | // Set build folder as static folder 39 | app.use(express.static(path.join(__dirname, '../frontend/build'))) 40 | 41 | app.get('*', (req, res) => { 42 | res.sendFile(path.join(__dirname, '../frontend/build/index.html')) 43 | }) 44 | } else { 45 | app.get('/', (req, res) => { 46 | res.status(200).json({ message: 'Welcome to the Support Desk API' }) 47 | }) 48 | } 49 | 50 | app.use(errorHandler) 51 | 52 | /** 53 | * app.listen() 54 | * Starts a UNIX socket and listens for connections on the given path. 55 | * This method is identical to Node’s http.Server.listen(). 56 | */ 57 | app.listen(PORT, () => { 58 | console.log(`Server is running on port ${PORT}`) 59 | }) 60 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "proxy": "http://localhost:5000/", 5 | "private": true, 6 | "dependencies": { 7 | "@reduxjs/toolkit": "^1.8.0", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.5.0", 10 | "@testing-library/user-event": "^7.2.1", 11 | "axios": "^0.26.0", 12 | "react": "^17.0.2", 13 | "react-dom": "^17.0.2", 14 | "react-icons": "^4.3.1", 15 | "react-modal": "^3.14.4", 16 | "react-redux": "^7.2.6", 17 | "react-router-dom": "^6.2.1", 18 | "react-scripts": "5.0.0", 19 | "react-toastify": "^8.2.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdarif/support-desk/82c85887884c89db437de340fe49c82aed3d2db7/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React Redux App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdarif/support-desk/82c85887884c89db437de340fe49c82aed3d2db7/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdarif/support-desk/82c85887884c89db437de340fe49c82aed3d2db7/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' 2 | import { ToastContainer } from 'react-toastify' 3 | import 'react-toastify/dist/ReactToastify.css' 4 | import Header from './components/Header' 5 | import { PrivateRoute } from './components/PrivateRoute' 6 | import Home from './pages/Home' 7 | import Login from './pages/Login' 8 | import NewTicket from './pages/NewTicket' 9 | import Register from './pages/Register' 10 | import Tickets from './pages/Tickets' 11 | import Ticket from './pages/Ticket' 12 | 13 | function App () { 14 | return ( 15 | <> 16 | 17 |
18 |
19 | 20 | } /> 21 | } /> 22 | } /> 23 | }> 24 | } /> 25 | 26 | }> 27 | } /> 28 | 29 | }> 30 | } /> 31 | 32 | 33 |
34 |
35 | 36 | 37 | ) 38 | } 39 | 40 | export default App 41 | -------------------------------------------------------------------------------- /frontend/src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import authReducer from '../features/auth/authSlice' 3 | import ticketReducer from '../features/tickets/ticketSlice' 4 | import noteReducer from '../features/notes/noteSlice' 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | auth: authReducer, 9 | tickets: ticketReducer, 10 | notes: noteReducer 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /frontend/src/components/BackButton.jsx: -------------------------------------------------------------------------------- 1 | import { FaArrowCircleLeft } from "react-icons/fa"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export const BackButton = ({ url }) => { 5 | return ( 6 | 7 | 8 | Back 9 | 10 | ); 11 | }; 12 | 13 | export default BackButton; 14 | -------------------------------------------------------------------------------- /frontend/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { FaSignOutAlt, FaSignInAlt, FaUser } from "react-icons/fa"; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { logout, reset } from "../features/auth/authSlice"; 5 | 6 | function Header() { 7 | const navigate = useNavigate(); 8 | const dispatch = useDispatch(); 9 | const { user } = useSelector((state) => state.auth); 10 | 11 | const onLogout = () => { 12 | dispatch(logout()); 13 | dispatch(reset()); 14 | navigate("/"); 15 | }; 16 | 17 | return ( 18 |
19 |
20 | Support Desk 21 |
22 |
    23 | {user ? ( 24 |
  • 25 | 29 |
  • 30 | ) : ( 31 | <> 32 |
  • 33 | 34 | Login 35 | 36 |
  • 37 |
  • 38 | 39 | Register 40 | 41 |
  • 42 | 43 | )} 44 |
45 |
46 | ); 47 | } 48 | 49 | export default Header; 50 | -------------------------------------------------------------------------------- /frontend/src/components/NoteItem.jsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | 3 | function NoteItem({ note }) { 4 | const { user } = useSelector((state) => state.auth); 5 | 6 | const options = { 7 | weekday: "long", 8 | year: "numeric", 9 | month: "long", 10 | day: "numeric", 11 | }; 12 | 13 | return ( 14 |
21 |

22 | Note from {note.isStaff ? Staff : {user.name}} 23 |

24 |

{note.text}

25 |
26 | {new Date(note.createdAt).toLocaleString("en-US", options)} 27 |
28 |
29 | ); 30 | } 31 | 32 | export default NoteItem; 33 | -------------------------------------------------------------------------------- /frontend/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from "react-router-dom"; 2 | import { useAuthStatus } from "../hooks/useAuthStatus"; 3 | import Spinner from "./Spinner"; 4 | 5 | export const PrivateRoute = () => { 6 | const { loggedIn, checkingStatus } = useAuthStatus(); 7 | 8 | if (checkingStatus) { 9 | return ; 10 | } 11 | 12 | return loggedIn ? : ; 13 | 14 | return
; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/components/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Spinner() { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | } 10 | 11 | export default Spinner; 12 | -------------------------------------------------------------------------------- /frontend/src/components/TicketItem.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | function TicketItem({ ticket }) { 5 | const options = { 6 | weekday: "long", 7 | year: "numeric", 8 | month: "long", 9 | day: "numeric", 10 | }; 11 | return ( 12 |
13 |
{new Date(ticket.createdAt).toLocaleString("en-US", options)}
14 |
{ticket.product}
15 |
{ticket.status}
16 | 17 | View 18 | 19 |
20 | ); 21 | } 22 | 23 | export default TicketItem; 24 | -------------------------------------------------------------------------------- /frontend/src/features/auth/authService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const API_URL = '/api/users' 4 | 5 | // Register user 6 | const register = async userData => { 7 | // userData is an object with an email and password i.e {name: 'Hina', email: 'hina@*****.com', password: '******'} 8 | const response = await axios.post(API_URL, userData) 9 | 10 | if (response.data) { 11 | // localStorage can only hold strings 12 | localStorage.setItem('user', JSON.stringify(response.data)) 13 | } 14 | 15 | return response.data 16 | } 17 | 18 | // Login user 19 | const login = async userData => { 20 | // userData is an object with an email and password i.e {email: 'arif@*****.com', password: '******'} 21 | const response = await axios.post(API_URL + '/login', userData) 22 | 23 | if (response.data) { 24 | // localStorage can only hold strings 25 | // Save the user data to localStorage in a key called 'user' 26 | localStorage.setItem('user', JSON.stringify(response.data)) 27 | } 28 | 29 | return response.data 30 | } 31 | 32 | // Logout user 33 | const logout = () => localStorage.removeItem('user') 34 | 35 | const authService = { 36 | register, 37 | logout, 38 | login 39 | } 40 | 41 | export default authService 42 | -------------------------------------------------------------------------------- /frontend/src/features/auth/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' 2 | import authService from './authService' 3 | 4 | // Get logged in user info from localStorage 5 | const user = JSON.parse(localStorage.getItem('user')) 6 | 7 | const initialState = { 8 | user: user ? user : null, 9 | isError: false, 10 | isLoading: false, 11 | isSuccess: false, 12 | message: '' 13 | } 14 | 15 | // Register new user 16 | /** 17 | * createAsyncThunk: A function that accepts a Redux action type string and 18 | * a callback function that should return a promise. 19 | * It generates promise lifecycle action types based 20 | * on the action type prefix that you pass in, 21 | * and returns a thunk action creator that will 22 | * run the promise callback and dispatch the 23 | * lifecycle actions based on the returned promise. 24 | * This abstracts the standard recommended approach for handling async request lifecycles. 25 | */ 26 | 27 | /** 28 | * 'auth/register' is the action type string in this case. 29 | * Whenever this function is dispatched from a component within our application, 30 | * 'createAsyncThunk' generates promise lifecycle action types using this 31 | * string as a prefix. 32 | * pending: auth/register/pending 33 | * fulfilled: auth/register/fulfilled 34 | * rejected: auth/register/rejected 35 | */ 36 | 37 | /** 38 | * On its initial call, 'createAsyncThunk' dispatches the auth/register/pending 39 | * lifecycle action type. The payloadCreator then executes to return either a 40 | * result or an error. 41 | * 42 | * In the event of an error, auth/register/rejected is dispatched and 43 | * 'createAsyncThunk' should either return a rejected promise containing 44 | * an Error instance, a plain descriptive message, 45 | * or a resolved promise with a RejectWithValue 46 | * argument as returned by the thunkAPI.rejectWithValue function. 47 | * 48 | * If our data fetch is successful, the posts/getPosts/fulfilled action 49 | * type gets dispatched. 50 | * 51 | */ 52 | 53 | export const register = createAsyncThunk( 54 | //action type string 55 | 'auth/register', 56 | 57 | // callback function 58 | async (user, thunkAPI) => { 59 | try { 60 | return await authService.register(user) 61 | } catch (error) { 62 | /** 63 | * Remember that when your 'payloadCreator' returns a rejected promise, 64 | * the 'rejected' action is dispatched (with 'action.payload' as 'undefined'). 65 | */ 66 | const message = 67 | (error.response && 68 | error.response.data && 69 | error.response.data.message) || 70 | error.message || 71 | error.toString() 72 | /** 73 | * By using 'thunkAPI', you can return a resolved promise to the reducer, 74 | * which has 'action.payload' set to a custom value of your choice. 75 | * 'thunkAPI' uses its 'rejectWithValue' property to perform this. 76 | */ 77 | return thunkAPI.rejectWithValue(message) 78 | } 79 | } 80 | ) 81 | 82 | // Login user 83 | export const login = createAsyncThunk('auth/login', async (user, thunkAPI) => { 84 | try { 85 | return await authService.login(user) 86 | } catch (error) { 87 | const message = 88 | (error.response && error.response.data && error.response.data.message) || 89 | error.message || 90 | error.toString() 91 | 92 | return thunkAPI.rejectWithValue(message) 93 | } 94 | }) 95 | 96 | // Logout user 97 | export const logout = createAsyncThunk('auth/logout', async () => { 98 | await authService.logout() 99 | }) 100 | 101 | /** 102 | * A Slice 'createSlice' function that accepts an initial state, an object of reducer functions, 103 | * and a "slice name", and automatically generates action creators and 104 | * action types that correspond to the reducers and state. 105 | */ 106 | /** 107 | * Within 'createSlice', synchronous requests made to the store are handled 108 | * in the reducers object while extraReducers handles asynchronous requests 109 | */ 110 | export const authSlice = createSlice({ 111 | // A name, used in action types 112 | name: 'auth', 113 | // The initial state for the reducer 114 | initialState, 115 | // An object of "case reducers". Key names will be used to generate actions. 116 | reducers: { 117 | reset: state => { 118 | state.isError = false 119 | state.isLoading = false 120 | state.isSuccess = false 121 | state.message = '' 122 | } 123 | }, 124 | /** 125 | * 126 | * extraReducers: One of the key concepts of Redux is that each slice reducer "owns" 127 | * its slice of state, and that many slice reducers can independently 128 | * respond to the same action type. extraReducers allows createSlice 129 | * to respond to other action types besides the types it has generated. 130 | * 131 | */ 132 | 133 | /** 134 | * 135 | * The three lifecycle action types mentioned earlier can then be evaluated 136 | * in extraReducers, where we make our desired changes to the store. 137 | * In this case 138 | */ 139 | extraReducers: builder => { 140 | builder 141 | .addCase(register.pending, state => { 142 | state.isLoading = true 143 | }) 144 | .addCase(register.fulfilled, (state, action) => { 145 | state.isLoading = false 146 | state.isSuccess = true 147 | state.user = action.payload 148 | }) 149 | .addCase(register.rejected, (state, action) => { 150 | state.isLoading = false 151 | state.isError = true 152 | state.message = action.payload 153 | state.user = null 154 | }) 155 | .addCase(login.pending, state => { 156 | state.isLoading = true 157 | }) 158 | .addCase(login.fulfilled, (state, action) => { 159 | state.isLoading = false 160 | state.isSuccess = true 161 | state.user = action.payload 162 | }) 163 | .addCase(login.rejected, (state, action) => { 164 | state.isLoading = false 165 | state.isError = true 166 | state.message = action.payload 167 | state.user = null 168 | }) 169 | .addCase(logout.fulfilled, state => { 170 | state.user = null 171 | }) 172 | } 173 | }) 174 | 175 | export const { reset } = authSlice.actions 176 | /** 177 | * Every slice you create must be added to your Redux store (src/app/store.js) 178 | * so you can gain access to its contents. 179 | */ 180 | export default authSlice.reducer 181 | -------------------------------------------------------------------------------- /frontend/src/features/notes/noteService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const API_URL = '/api/tickets/' 4 | 5 | // Get ticket notes 6 | const getNotes = async (ticketId, token) => { 7 | const config = { 8 | headers: { 9 | Authorization: `Bearer ${token}` 10 | } 11 | } 12 | 13 | const response = await axios.get(API_URL + ticketId + '/notes', config) 14 | 15 | return response.data 16 | } 17 | 18 | // Create ticket note 19 | const createNote = async (noteText, ticketId, token) => { 20 | const config = { 21 | headers: { 22 | Authorization: `Bearer ${token}` 23 | } 24 | } 25 | 26 | const response = await axios.post( 27 | API_URL + ticketId + '/notes', 28 | { 29 | text: noteText 30 | }, 31 | config 32 | ) 33 | 34 | return response.data 35 | } 36 | 37 | const noteService = { 38 | getNotes, 39 | createNote 40 | } 41 | 42 | export default noteService 43 | -------------------------------------------------------------------------------- /frontend/src/features/notes/noteSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' 2 | import noteService from './noteService' 3 | 4 | const initialState = { 5 | notes: [], 6 | isError: false, 7 | isSuccess: false, 8 | isLoading: false, 9 | message: '' 10 | } 11 | 12 | // Get ticket notes 13 | /** 14 | * createAsyncThunk: A function that accepts a Redux action type string and 15 | * a callback function that should return a promise. 16 | * It generates promise lifecycle action types based 17 | * on the action type prefix that you pass in, 18 | * and returns a thunk action creator that will 19 | * run the promise callback and dispatch the 20 | * lifecycle actions based on the returned promise. 21 | * This abstracts the standard recommended approach for handling async request lifecycles. 22 | */ 23 | export const getNotes = createAsyncThunk( 24 | 'tickets/getAll', 25 | async (ticketId, thunkAPI) => { 26 | /** 27 | * thunkAPI: an object containing all of the parameters 28 | * that are normally passed to a Redux thunk function, 29 | * as well as additional options: https://redux-toolkit.js.org/api/createAsyncThunk 30 | */ 31 | try { 32 | // Token is required for authentication 33 | const token = thunkAPI.getState().auth.user.token 34 | return await noteService.getNotes(ticketId, token) 35 | } catch (error) { 36 | const message = 37 | (error.response && 38 | error.response.data && 39 | error.response.data.message) || 40 | error.message || 41 | error.toString() 42 | 43 | return thunkAPI.rejectWithValue(message) 44 | } 45 | } 46 | ) 47 | 48 | // Creata ticket note 49 | /** 50 | * createAsyncThunk: A function that accepts a Redux action type string and 51 | * a callback function that should return a promise. 52 | * It generates promise lifecycle action types based 53 | * on the action type prefix that you pass in, 54 | * and returns a thunk action creator that will 55 | * run the promise callback and dispatch the 56 | * lifecycle actions based on the returned promise. 57 | * This abstracts the standard recommended approach for handling async request lifecycles. 58 | */ 59 | export const createNote = createAsyncThunk( 60 | 'notes/create', 61 | async ({ noteText, ticketId }, thunkAPI) => { 62 | /** 63 | * thunkAPI: an object containing all of the parameters 64 | * that are normally passed to a Redux thunk function, 65 | * as well as additional options: https://redux-toolkit.js.org/api/createAsyncThunk 66 | */ 67 | try { 68 | // Token is required for authentication 69 | const token = thunkAPI.getState().auth.user.token 70 | return await noteService.createNote(noteText, ticketId, token) 71 | } catch (error) { 72 | const message = 73 | (error.response && 74 | error.response.data && 75 | error.response.data.message) || 76 | error.message || 77 | error.toString() 78 | 79 | return thunkAPI.rejectWithValue(message) 80 | } 81 | } 82 | ) 83 | 84 | export const noteSlice = createSlice({ 85 | name: 'note', 86 | initialState, 87 | reducers: { 88 | reset: state => initialState 89 | }, 90 | extraReducers: builder => { 91 | builder 92 | .addCase(getNotes.pending, state => { 93 | state.isLoading = true 94 | }) 95 | .addCase(getNotes.fulfilled, (state, action) => { 96 | state.isLoading = false 97 | state.isSuccess = true 98 | state.notes = action.payload 99 | }) 100 | .addCase(getNotes.rejected, (state, action) => { 101 | state.isLoading = false 102 | state.isError = true 103 | state.message = action.payload 104 | }) 105 | .addCase(createNote.pending, state => { 106 | state.isLoading = true 107 | }) 108 | .addCase(createNote.fulfilled, (state, action) => { 109 | state.isLoading = false 110 | state.isSuccess = true 111 | state.notes.push(action.payload) 112 | }) 113 | .addCase(createNote.rejected, (state, action) => { 114 | state.isLoading = false 115 | state.isError = true 116 | state.message = action.payload 117 | }) 118 | } 119 | }) 120 | 121 | export const { reset } = noteSlice.actions 122 | export default noteSlice.reducer 123 | -------------------------------------------------------------------------------- /frontend/src/features/tickets/ticketService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const API_URL = '/api/tickets/' 4 | 5 | // Create new ticket 6 | const createTicket = async (ticketData, token) => { 7 | const config = { 8 | headers: { 9 | Authorization: `Bearer ${token}` 10 | } 11 | } 12 | 13 | const response = await axios.post(API_URL, ticketData, config) 14 | 15 | return response.data 16 | } 17 | 18 | // Get user tickets 19 | const getTickets = async token => { 20 | const config = { 21 | headers: { 22 | Authorization: `Bearer ${token}` 23 | } 24 | } 25 | 26 | const response = await axios.get(API_URL, config) 27 | 28 | return response.data 29 | } 30 | 31 | // Get user ticket 32 | const getTicket = async (ticketId, token) => { 33 | const config = { 34 | headers: { 35 | Authorization: `Bearer ${token}` 36 | } 37 | } 38 | 39 | const response = await axios.get(API_URL + ticketId, config) 40 | 41 | return response.data 42 | } 43 | 44 | // Close ticket 45 | const closeTicket = async (ticketId, token) => { 46 | const config = { 47 | headers: { 48 | Authorization: `Bearer ${token}` 49 | } 50 | } 51 | 52 | const response = await axios.put( 53 | API_URL + ticketId, 54 | { status: 'close' }, 55 | config 56 | ) 57 | 58 | return response.data 59 | } 60 | 61 | const ticketService = { 62 | createTicket, 63 | getTickets, 64 | getTicket, 65 | closeTicket 66 | } 67 | 68 | export default ticketService 69 | -------------------------------------------------------------------------------- /frontend/src/features/tickets/ticketSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' 2 | import ticketService from './ticketService' 3 | 4 | const initialState = { 5 | tickets: [], 6 | ticket: {}, 7 | isError: false, 8 | isSuccess: false, 9 | isLoading: false, 10 | message: '' 11 | } 12 | 13 | // Create new ticket 14 | /** 15 | * createAsyncThunk: A function that accepts a Redux action type string and 16 | * a callback function that should return a promise. 17 | * It generates promise lifecycle action types based 18 | * on the action type prefix that you pass in, 19 | * and returns a thunk action creator that will 20 | * run the promise callback and dispatch the 21 | * lifecycle actions based on the returned promise. 22 | * This abstracts the standard recommended approach for handling async request lifecycles. 23 | */ 24 | export const createTicket = createAsyncThunk( 25 | 'tickets/create', 26 | async (ticketData, thunkAPI) => { 27 | /** 28 | * thunkAPI: an object containing all of the parameters 29 | * that are normally passed to a Redux thunk function, 30 | * as well as additional options: https://redux-toolkit.js.org/api/createAsyncThunk 31 | */ 32 | try { 33 | // Token is required for authentication 34 | const token = thunkAPI.getState().auth.user.token 35 | return await ticketService.createTicket(ticketData, token) 36 | } catch (error) { 37 | const message = 38 | (error.response && 39 | error.response.data && 40 | error.response.data.message) || 41 | error.message || 42 | error.toString() 43 | 44 | return thunkAPI.rejectWithValue(message) 45 | } 46 | } 47 | ) 48 | 49 | // Get user tickets 50 | export const getTickets = createAsyncThunk( 51 | 'tickets/getAll', 52 | async (_, thunkAPI) => { 53 | /** 54 | * thunkAPI: an object containing all of the parameters 55 | * that are normally passed to a Redux thunk function, 56 | * as well as additional options: https://redux-toolkit.js.org/api/createAsyncThunk 57 | */ 58 | try { 59 | // Token is required for authentication 60 | const token = thunkAPI.getState().auth.user.token 61 | return await ticketService.getTickets(token) 62 | } catch (error) { 63 | const message = 64 | (error.response && 65 | error.response.data && 66 | error.response.data.message) || 67 | error.message || 68 | error.toString() 69 | 70 | return thunkAPI.rejectWithValue(message) 71 | } 72 | } 73 | ) 74 | 75 | // Get user ticket 76 | export const getTicket = createAsyncThunk( 77 | 'tickets/get', 78 | async (ticketId, thunkAPI) => { 79 | /** 80 | * thunkAPI: an object containing all of the parameters 81 | * that are normally passed to a Redux thunk function, 82 | * as well as additional options: https://redux-toolkit.js.org/api/createAsyncThunk 83 | */ 84 | try { 85 | // Token is required for authentication 86 | const token = thunkAPI.getState().auth.user.token 87 | return await ticketService.getTicket(ticketId, token) 88 | } catch (error) { 89 | const message = 90 | (error.response && 91 | error.response.data && 92 | error.response.data.message) || 93 | error.message || 94 | error.toString() 95 | 96 | return thunkAPI.rejectWithValue(message) 97 | } 98 | } 99 | ) 100 | 101 | // Close ticket 102 | export const closeTicket = createAsyncThunk( 103 | 'tickets/close', 104 | async (ticketId, thunkAPI) => { 105 | /** 106 | * thunkAPI: an object containing all of the parameters 107 | * that are normally passed to a Redux thunk function, 108 | * as well as additional options: https://redux-toolkit.js.org/api/createAsyncThunk 109 | */ 110 | try { 111 | // Token is required for authentication 112 | const token = thunkAPI.getState().auth.user.token 113 | return await ticketService.closeTicket(ticketId, token) // Service function 114 | } catch (error) { 115 | const message = 116 | (error.response && 117 | error.response.data && 118 | error.response.data.message) || 119 | error.message || 120 | error.toString() 121 | 122 | return thunkAPI.rejectWithValue(message) 123 | } 124 | } 125 | ) 126 | 127 | export const ticketSlice = createSlice({ 128 | name: 'ticket', 129 | initialState, 130 | reducers: { 131 | reset: state => initialState 132 | }, 133 | extraReducers: builder => { 134 | builder 135 | .addCase(createTicket.pending, state => { 136 | state.isLoading = true 137 | }) 138 | .addCase(createTicket.fulfilled, state => { 139 | state.isLoading = false 140 | state.isSuccess = true 141 | }) 142 | .addCase(createTicket.rejected, (state, action) => { 143 | state.isLoading = false 144 | state.isError = true 145 | state.message = action.payload 146 | }) 147 | .addCase(getTickets.pending, state => { 148 | state.isLoading = true 149 | }) 150 | .addCase(getTickets.fulfilled, (state, action) => { 151 | state.isLoading = false 152 | state.isSuccess = true 153 | state.tickets = action.payload 154 | }) 155 | .addCase(getTickets.rejected, (state, action) => { 156 | state.isLoading = false 157 | state.isError = true 158 | state.message = action.payload 159 | }) 160 | .addCase(getTicket.pending, state => { 161 | state.isLoading = true 162 | }) 163 | .addCase(getTicket.fulfilled, (state, action) => { 164 | state.isLoading = false 165 | state.isSuccess = true 166 | state.ticket = action.payload 167 | }) 168 | .addCase(getTicket.rejected, (state, action) => { 169 | state.isLoading = false 170 | state.isError = true 171 | state.message = action.payload 172 | }) 173 | .addCase(closeTicket.fulfilled, (state, action) => { 174 | state.isLoading = false 175 | state.tickets.map(ticket => 176 | ticket._id === action.payload._id ? (ticket.status = 'close') : ticket 177 | ) 178 | }) 179 | } 180 | }) 181 | 182 | export const { reset } = ticketSlice.actions 183 | export default ticketSlice.reducer 184 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuthStatus.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { useSelector } from 'react-redux' 3 | 4 | export const useAuthStatus = () => { 5 | const [loggedIn, setLoggedIn] = useState(false) 6 | const [checkingStatus, setCheckingStatus] = useState(true) 7 | 8 | const { user } = useSelector(state => state.auth) 9 | 10 | useEffect(() => { 11 | if (user) { 12 | setLoggedIn(true) 13 | } else { 14 | setLoggedIn(false) 15 | } 16 | setCheckingStatus(false) 17 | }, [user]) 18 | return { loggedIn, checkingStatus } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | font-family: 'Poppins', sans-serif; 11 | height: 100vh; 12 | } 13 | 14 | a { 15 | text-decoration: none; 16 | color: #000; 17 | } 18 | 19 | p { 20 | line-height: 1.7; 21 | } 22 | 23 | ul { 24 | list-style: none; 25 | } 26 | 27 | li { 28 | line-height: 2.2; 29 | } 30 | 31 | h1, 32 | h2, 33 | h3 { 34 | font-weight: 600; 35 | margin-bottom: 10px; 36 | } 37 | 38 | .container { 39 | width: 100%; 40 | max-width: 960px; 41 | margin: 0 auto; 42 | padding: 0 20px; 43 | text-align: center; 44 | } 45 | 46 | .header { 47 | display: flex; 48 | justify-content: space-between; 49 | align-items: center; 50 | padding: 20px 0; 51 | border-bottom: 1px solid #e6e6e6; 52 | margin-bottom: 60px; 53 | } 54 | 55 | .header ul { 56 | display: flex; 57 | align-items: center; 58 | justify-content: space-between; 59 | } 60 | 61 | .header ul li { 62 | margin-left: 20px; 63 | } 64 | 65 | .header ul li a { 66 | display: flex; 67 | align-items: center; 68 | } 69 | 70 | .header ul li a:hover { 71 | color: #777; 72 | } 73 | 74 | .header ul li a svg { 75 | margin-right: 5px; 76 | } 77 | 78 | .heading { 79 | font-size: 2rem; 80 | font-weight: 700; 81 | margin-bottom: 50px; 82 | padding: 0 20px; 83 | } 84 | 85 | .heading p { 86 | color: #828282; 87 | } 88 | 89 | .boxes { 90 | display: grid; 91 | grid-template-columns: repeat(3, 1fr); 92 | gap: 20px; 93 | justify-content: space-between; 94 | align-items: center; 95 | text-align: center; 96 | margin-bottom: 30px; 97 | } 98 | 99 | .boxes div { 100 | padding: 30px; 101 | border: 1px solid #e6e6e6; 102 | border-radius: 10px; 103 | } 104 | 105 | .boxes h2 { 106 | margin-top: 20px; 107 | } 108 | 109 | .boxes a:hover { 110 | color: #777; 111 | } 112 | 113 | .form { 114 | width: 70%; 115 | margin: 0 auto; 116 | } 117 | 118 | .form-group { 119 | margin-bottom: 10px; 120 | } 121 | 122 | .form-group input, 123 | .form-group textarea, 124 | .form-group select { 125 | width: 100%; 126 | padding: 10px; 127 | border: 1px solid #e6e6e6; 128 | border-radius: 5px; 129 | margin-bottom: 10px; 130 | font-family: inherit; 131 | } 132 | 133 | .form-group label { 134 | text-align: left; 135 | display: block; 136 | margin: 0 0 5px 3px; 137 | } 138 | 139 | .btn { 140 | padding: 10px 20px; 141 | border: 1px solid #000; 142 | border-radius: 5px; 143 | background: #000; 144 | color: #fff; 145 | font-size: 16px; 146 | font-weight: 700; 147 | cursor: pointer; 148 | text-align: center; 149 | appearance: button; 150 | display: flex; 151 | align-items: center; 152 | justify-content: center; 153 | } 154 | 155 | .btn svg { 156 | margin-right: 8px; 157 | } 158 | 159 | .btn-reverse { 160 | background: #fff; 161 | color: #000; 162 | } 163 | 164 | .btn-block { 165 | width: 100%; 166 | margin-bottom: 20px; 167 | } 168 | 169 | .btn-sm { 170 | padding: 5px 15px; 171 | font-size: 13px; 172 | } 173 | 174 | .btn-danger { 175 | background: darkred; 176 | border: none; 177 | } 178 | 179 | .btn-back { 180 | width: 150px; 181 | margin-bottom: 20px; 182 | } 183 | 184 | .btn:hover { 185 | transform: scale(0.98); 186 | } 187 | 188 | .ticket-created { 189 | border: 1px solid #e6e6e6; 190 | border-radius: 5px; 191 | padding: 50px; 192 | } 193 | 194 | .ticket-number { 195 | margin-bottom: 30px; 196 | } 197 | 198 | .ticket-number h2 { 199 | font-size: 2.3rem; 200 | margin-bottom: 10px; 201 | } 202 | 203 | .ticket-number p { 204 | font-size: 1.3rem; 205 | } 206 | 207 | .ticket-info { 208 | font-size: 1.3rem; 209 | } 210 | 211 | .ticket, 212 | .ticket-headings { 213 | display: grid; 214 | grid-template-columns: repeat(4, 1fr); 215 | gap: 20px; 216 | justify-content: space-between; 217 | align-items: center; 218 | margin-bottom: 20px; 219 | background: #f4f4f4; 220 | padding: 10px 15px; 221 | border-radius: 5px; 222 | text-align: center; 223 | } 224 | 225 | .ticket-headings { 226 | font-weight: 700; 227 | } 228 | 229 | .status { 230 | background-color: #333; 231 | color: #fff; 232 | width: 100px; 233 | padding: 0 20px; 234 | justify-self: center; 235 | border-radius: 10px; 236 | font-size: 16px; 237 | text-align: center; 238 | } 239 | 240 | .status-new { 241 | background-color: green; 242 | color: #fff; 243 | border-radius: 10px; 244 | } 245 | 246 | .status-open { 247 | background-color: steelblue; 248 | color: #fff; 249 | border-radius: 10px; 250 | } 251 | 252 | .status-close { 253 | background-color: darkred; 254 | color: #fff; 255 | border-radius: 10px; 256 | } 257 | 258 | .ticket-page { 259 | position: relative; 260 | text-align: left; 261 | } 262 | 263 | .ticket-page h2 { 264 | display: flex; 265 | align-items: center; 266 | justify-content: space-between; 267 | } 268 | 269 | .ticket-page .btn { 270 | margin-bottom: 30px; 271 | } 272 | 273 | .ticket-page .btn-block { 274 | width: 100%; 275 | margin-top: 30px; 276 | } 277 | 278 | .ticket-desc { 279 | margin: 20px 0; 280 | font-size: 17px; 281 | background-color: #f4f4f4; 282 | border: 1px #ccc solid; 283 | padding: 10px 15px; 284 | border-radius: 5px; 285 | } 286 | 287 | .note { 288 | border: 1px solid #e6e6e6; 289 | border-radius: 5px; 290 | text-align: left; 291 | padding: 20px; 292 | margin-bottom: 20px; 293 | position: relative; 294 | } 295 | 296 | .note-head { 297 | background: #f4f4f4; 298 | padding: 5px 20px; 299 | display: flex; 300 | justify-content: space-between; 301 | align-items: center; 302 | margin-bottom: 20px; 303 | } 304 | 305 | .note-date { 306 | position: absolute; 307 | top: 15px; 308 | right: 10px; 309 | font-size: 14px; 310 | } 311 | 312 | .delete-note { 313 | color: red; 314 | cursor: pointer; 315 | position: absolute; 316 | bottom: 10px; 317 | right: 20px; 318 | } 319 | 320 | .btn-close { 321 | background: none; 322 | border: none; 323 | color: #000; 324 | position: absolute; 325 | top: 5px; 326 | right: 5px; 327 | font-size: 16px; 328 | cursor: pointer; 329 | } 330 | 331 | .btn-close:hover { 332 | color: red; 333 | transform: scale(0.98); 334 | } 335 | 336 | p.status-in-progress { 337 | color: orangered; 338 | } 339 | 340 | p.status-waiting { 341 | color: red; 342 | } 343 | 344 | p.status-ready { 345 | color: steelblue; 346 | } 347 | 348 | p.status-complete { 349 | color: green; 350 | } 351 | 352 | footer { 353 | position: sticky; 354 | top: 95vh; 355 | text-align: center; 356 | } 357 | 358 | .loadingSpinnerContainer { 359 | position: fixed; 360 | top: 0; 361 | right: 0; 362 | bottom: 0; 363 | left: 0; 364 | background-color: rgba(0, 0, 0, 0.5); 365 | z-index: 5000; 366 | display: flex; 367 | justify-content: center; 368 | align-items: center; 369 | } 370 | 371 | .loadingSpinner { 372 | width: 64px; 373 | height: 64px; 374 | border: 8px solid; 375 | border-color: #000 transparent #555 transparent; 376 | border-radius: 50%; 377 | animation: spin 1.2s linear infinite; 378 | } 379 | 380 | @keyframes spin { 381 | 0% { 382 | transform: rotate(0deg); 383 | } 384 | 100% { 385 | transform: rotate(360deg); 386 | } 387 | } 388 | 389 | @media (max-width: 600px) { 390 | .boxes { 391 | grid-template-columns: 1fr; 392 | } 393 | 394 | .form { 395 | width: 90%; 396 | } 397 | 398 | .ticket-created h2, 399 | .heading h1 { 400 | font-size: 2rem; 401 | } 402 | 403 | .heading p { 404 | font-size: 1.5rem; 405 | } 406 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { store } from './app/store'; 6 | import { Provider } from 'react-redux'; 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /frontend/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import { FaQuestionCircle, FaTicketAlt } from "react-icons/fa"; 2 | import { Link } from "react-router-dom"; 3 | 4 | function Home() { 5 | return ( 6 | <> 7 |
8 |

What do you need help with?

9 |

Please choose from an option below

10 |
11 | 12 | 13 | Create New Ticket 14 | 15 | 16 | 17 | View My Tickets 18 | 19 | 20 | ); 21 | } 22 | 23 | export default Home; 24 | -------------------------------------------------------------------------------- /frontend/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { FaSignInAlt } from "react-icons/fa"; 5 | import { useSelector, useDispatch } from "react-redux"; 6 | import { login, reset } from "../features/auth/authSlice"; 7 | import Spinner from "../components/Spinner"; 8 | 9 | function Login() { 10 | const [formData, setFormData] = useState({ 11 | email: "", 12 | password: "", 13 | }); 14 | 15 | const { email, password } = formData; // destructuring 16 | 17 | /** 18 | * useDispatch() returns a reference to the dispatch function 19 | * from the Redux store. You may use it to dispatch actions 20 | * as needed. 21 | */ 22 | const dispatch = useDispatch(); 23 | const navigate = useNavigate(); 24 | 25 | /** 26 | * useSelector() allows you to extract data from the Redux store state, 27 | * using a selector function. 28 | */ 29 | 30 | const { user, isLoading, isError, isSuccess, message } = useSelector( 31 | (state) => state.auth 32 | ); 33 | 34 | useEffect(() => { 35 | if (isError) { 36 | toast.error(message); 37 | } 38 | 39 | // Redirect when logged in 40 | if (isSuccess || user) { 41 | navigate("/"); 42 | } 43 | 44 | dispatch(reset()); 45 | }, [isError, isSuccess, user, navigate, message, dispatch]); 46 | 47 | const onChange = (e) => { 48 | setFormData((prevState) => ({ 49 | ...prevState, 50 | [e.target.name]: e.target.value, 51 | })); 52 | }; 53 | 54 | const onSubmit = (e) => { 55 | e.preventDefault(); 56 | 57 | const userData = { 58 | email, 59 | password, 60 | }; 61 | 62 | dispatch(login(userData)); 63 | }; 64 | 65 | if (isLoading) { 66 | return ; 67 | } 68 | 69 | return ( 70 | <> 71 |
72 |

73 | Login 74 |

75 |

Please log in to get support

76 |
77 | 78 |
79 |
80 |
81 | 91 |
92 |
93 | 103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 | ); 111 | } 112 | 113 | export default Login; 114 | -------------------------------------------------------------------------------- /frontend/src/pages/NewTicket.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { toast } from "react-toastify"; 5 | import { createTicket, reset } from "../features/tickets/ticketSlice"; 6 | import Spinner from "../components/Spinner"; 7 | import BackButton from "../components/BackButton"; 8 | 9 | function NewTicket() { 10 | const { user } = useSelector((state) => state.auth); 11 | const { isLoading, isError, isSuccess, message } = useSelector( 12 | (state) => state.tickets 13 | ); 14 | 15 | const [name] = useState(user.name); 16 | const [email] = useState(user.email); 17 | const [product, setProduct] = useState("iPhone"); 18 | const [description, setDescription] = useState(""); 19 | 20 | const dispatch = useDispatch(); 21 | const navigate = useNavigate(); 22 | 23 | useEffect(() => { 24 | if (isError) { 25 | toast.error(message); 26 | } 27 | 28 | if (isSuccess) { 29 | dispatch(reset()); 30 | navigate("/tickets"); 31 | } 32 | }, [dispatch, isError, isSuccess, navigate, message]); 33 | 34 | const onSubmit = (e) => { 35 | e.preventDefault(); 36 | dispatch(createTicket({ product, description })); 37 | }; 38 | 39 | if (isLoading) return ; 40 | 41 | return ( 42 | <> 43 | 44 |
45 |

Create New Ticket

46 |

Please fill out the form below

47 |
48 | 49 |
50 |
51 | 52 | 53 |
54 |
55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 |
63 | 64 | 75 |
76 |
77 | 78 | 86 |
87 |
88 | 89 |
90 |
91 |
92 | 93 | ); 94 | } 95 | 96 | export default NewTicket; 97 | -------------------------------------------------------------------------------- /frontend/src/pages/Register.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { toast } from "react-toastify"; 4 | import { FaUser } from "react-icons/fa"; 5 | import { useSelector, useDispatch } from "react-redux"; 6 | import { register, reset } from "../features/auth/authSlice"; 7 | import Spinner from "../components/Spinner"; 8 | 9 | function Register() { 10 | const [formData, setFormData] = useState({ 11 | name: "", 12 | email: "", 13 | password: "", 14 | password2: "", 15 | }); 16 | 17 | const { name, email, password, password2 } = formData; // destructuring 18 | 19 | /** 20 | * useDispatch() returns a reference to the dispatch function 21 | * from the Redux store. You may use it to dispatch actions 22 | * as needed. 23 | */ 24 | 25 | /** 26 | * By using 'useSelector' and 'useDispatch' from 'react-redux', 27 | * we can read state from a Redux store and dispatch any 28 | * action from a component, respectively. 29 | */ 30 | const dispatch = useDispatch(); 31 | 32 | const navigate = useNavigate(); 33 | 34 | /** 35 | * useSelector() allows you to extract data from the Redux store state, 36 | * using a selector function. 37 | */ 38 | const { user, isLoading, isError, isSuccess, message } = useSelector( 39 | (state) => state.auth 40 | ); 41 | 42 | useEffect(() => { 43 | if (isError) { 44 | toast.error(message); 45 | } 46 | 47 | // Redirect when logged in 48 | if (isSuccess || user) { 49 | navigate("/"); 50 | } 51 | 52 | dispatch(reset()); 53 | }, [isError, isSuccess, user, navigate, message, dispatch]); 54 | 55 | const onChange = (e) => { 56 | setFormData((prevState) => ({ 57 | ...prevState, 58 | [e.target.name]: e.target.value, 59 | })); 60 | }; 61 | 62 | const onSubmit = (e) => { 63 | e.preventDefault(); 64 | 65 | if (password !== password2) { 66 | toast.error("Passwords do not match"); 67 | } else { 68 | const userData = { 69 | name, 70 | email, 71 | password, 72 | }; 73 | 74 | dispatch(register(userData)); 75 | } 76 | }; 77 | 78 | if (isLoading) { 79 | return ; 80 | } 81 | 82 | return ( 83 | <> 84 |
85 |

86 | Register 87 |

88 |

Please create an account

89 |
90 | 91 |
92 |
93 |
94 | 104 |
105 |
106 | 116 |
117 |
118 | 128 |
129 |
130 | 140 |
141 |
142 | 143 |
144 |
145 |
146 | 147 | ); 148 | } 149 | 150 | export default Register; 151 | -------------------------------------------------------------------------------- /frontend/src/pages/Ticket.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import BackButton from "../components/BackButton"; 3 | import { getTicket, closeTicket } from "../features/tickets/ticketSlice"; 4 | import { 5 | getNotes, 6 | createNote, 7 | reset as notesReset, 8 | } from "../features/notes/noteSlice"; 9 | import Spinner from "../components/Spinner"; 10 | import { useParams, useNavigate } from "react-router-dom"; 11 | import { useEffect, useState } from "react"; 12 | import { toast } from "react-toastify"; 13 | import NoteItem from "../components/NoteItem"; 14 | import Modal from "react-modal"; 15 | import { FaPlus } from "react-icons/fa"; 16 | 17 | const customStyles = { 18 | content: { 19 | width: "600px", 20 | top: "50%", 21 | left: "50%", 22 | right: "auto", 23 | bottom: "auto", 24 | marginRight: "-50%", 25 | transform: "translate(-50%, -50%)", 26 | position: "relative", 27 | }, 28 | }; 29 | 30 | Modal.setAppElement("#root"); 31 | 32 | function Ticket() { 33 | const options = { 34 | weekday: "long", 35 | year: "numeric", 36 | month: "long", 37 | day: "numeric", 38 | }; 39 | const [modalIsOpen, setModalIsOpen] = useState(false); 40 | const [noteText, setNoteText] = useState(""); 41 | 42 | const { ticket, isLoading, isError, message } = useSelector( 43 | (state) => state.tickets 44 | ); 45 | 46 | const { notes, isLoading: notesIsLoading } = useSelector( 47 | (state) => state.notes 48 | ); 49 | 50 | const { ticketId } = useParams(); 51 | const dispatch = useDispatch(); 52 | const navigate = useNavigate(); 53 | 54 | useEffect(() => { 55 | if (isError) { 56 | toast.error(message); 57 | } 58 | 59 | dispatch(getTicket(ticketId)); 60 | dispatch(getNotes(ticketId)); 61 | // eslint-disable-next-line 62 | }, [isError, message, ticketId]); 63 | 64 | if (isLoading || notesIsLoading) return ; 65 | 66 | if (isError) { 67 | return

Something went wrong

; 68 | } 69 | 70 | // Close ticket 71 | const onTicketClose = () => { 72 | dispatch(closeTicket(ticketId)); 73 | toast.success("Ticket Closed"); 74 | navigate("/tickets"); 75 | }; 76 | 77 | // Open/Close Modal 78 | const openModal = () => { 79 | setModalIsOpen(true); 80 | }; 81 | const closeModal = () => { 82 | setModalIsOpen(false); 83 | }; 84 | 85 | // Create Note Submit 86 | const onNoteSubmit = (e) => { 87 | e.preventDefault(); 88 | dispatch(createNote({ ticketId, noteText })); 89 | closeModal(); 90 | }; 91 | 92 | return ( 93 |
94 |
95 | 96 |

97 | Ticket ID: {ticket._id} 98 | 99 | {ticket.status} 100 | 101 |

102 |

103 | Date Submitted:{" "} 104 | {new Date(ticket.createdAt).toLocaleString("en-US", options)} 105 |

106 |

Product: {ticket.product}

107 |
108 |
109 |

Description of Issue

110 |

{ticket.description}

111 |
112 |

Notes

113 |
114 | 115 | {ticket.status !== "close" && ( 116 | 119 | )} 120 | 121 | 127 |

Add Note

128 | 131 |
132 |
133 | 141 |
142 |
143 | 146 |
147 |
148 |
149 | 150 | {notes.map((note) => ( 151 | 152 | ))} 153 | 154 | {ticket.status !== "close" && ( 155 | 158 | )} 159 |
160 | ); 161 | } 162 | 163 | export default Ticket; 164 | -------------------------------------------------------------------------------- /frontend/src/pages/Tickets.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import Spinner from "../components/Spinner"; 4 | import BackButton from "../components/BackButton"; 5 | import { getTickets, reset } from "../features/tickets/ticketSlice"; 6 | import TicketItem from "../components/TicketItem"; 7 | 8 | function Tickets() { 9 | const { tickets, isLoading, isSuccess } = useSelector( 10 | (state) => state.tickets 11 | ); 12 | 13 | const dispatch = useDispatch(); 14 | 15 | useEffect(() => { 16 | // Unmounting 17 | return () => { 18 | if (isSuccess) { 19 | dispatch(reset()); 20 | } 21 | }; 22 | }, [dispatch, isSuccess]); 23 | 24 | useEffect(() => { 25 | dispatch(getTickets()); 26 | }, [dispatch]); 27 | 28 | if (isLoading) return ; 29 | 30 | return ( 31 | <> 32 | 33 |

Tickets

34 |
35 |
36 |
Date
37 |
Product
38 |
Status
39 |
40 |
41 | {tickets.map((ticket) => ( 42 | 43 | ))} 44 |
45 | 46 | ); 47 | } 48 | 49 | export default Tickets; 50 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then((registration) => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "support-desk", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "support-desk", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "bcryptjs": "^2.4.3", 13 | "colors": "^1.4.0", 14 | "concurrently": "^7.0.0", 15 | "dotenv": "^16.0.0", 16 | "express": "^4.17.3", 17 | "jsonwebtoken": "^8.5.1", 18 | "mongoose": "^6.2.3" 19 | }, 20 | "devDependencies": { 21 | "express-async-handler": "^1.2.0", 22 | "nodeman": "^1.1.2" 23 | } 24 | }, 25 | "node_modules/@types/node": { 26 | "version": "17.0.21", 27 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", 28 | "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==" 29 | }, 30 | "node_modules/@types/webidl-conversions": { 31 | "version": "6.1.1", 32 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz", 33 | "integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q==" 34 | }, 35 | "node_modules/@types/whatwg-url": { 36 | "version": "8.2.1", 37 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz", 38 | "integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==", 39 | "dependencies": { 40 | "@types/node": "*", 41 | "@types/webidl-conversions": "*" 42 | } 43 | }, 44 | "node_modules/accepts": { 45 | "version": "1.3.8", 46 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 47 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 48 | "dependencies": { 49 | "mime-types": "~2.1.34", 50 | "negotiator": "0.6.3" 51 | }, 52 | "engines": { 53 | "node": ">= 0.6" 54 | } 55 | }, 56 | "node_modules/ansi-regex": { 57 | "version": "5.0.1", 58 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 59 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 60 | "engines": { 61 | "node": ">=8" 62 | } 63 | }, 64 | "node_modules/ansi-styles": { 65 | "version": "4.3.0", 66 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 67 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 68 | "dependencies": { 69 | "color-convert": "^2.0.1" 70 | }, 71 | "engines": { 72 | "node": ">=8" 73 | }, 74 | "funding": { 75 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 76 | } 77 | }, 78 | "node_modules/array-flatten": { 79 | "version": "1.1.1", 80 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 81 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 82 | }, 83 | "node_modules/base64-js": { 84 | "version": "1.5.1", 85 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 86 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 87 | "funding": [ 88 | { 89 | "type": "github", 90 | "url": "https://github.com/sponsors/feross" 91 | }, 92 | { 93 | "type": "patreon", 94 | "url": "https://www.patreon.com/feross" 95 | }, 96 | { 97 | "type": "consulting", 98 | "url": "https://feross.org/support" 99 | } 100 | ] 101 | }, 102 | "node_modules/bcryptjs": { 103 | "version": "2.4.3", 104 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 105 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 106 | }, 107 | "node_modules/body-parser": { 108 | "version": "1.19.2", 109 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", 110 | "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", 111 | "dependencies": { 112 | "bytes": "3.1.2", 113 | "content-type": "~1.0.4", 114 | "debug": "2.6.9", 115 | "depd": "~1.1.2", 116 | "http-errors": "1.8.1", 117 | "iconv-lite": "0.4.24", 118 | "on-finished": "~2.3.0", 119 | "qs": "6.9.7", 120 | "raw-body": "2.4.3", 121 | "type-is": "~1.6.18" 122 | }, 123 | "engines": { 124 | "node": ">= 0.8" 125 | } 126 | }, 127 | "node_modules/bson": { 128 | "version": "4.6.1", 129 | "resolved": "https://registry.npmjs.org/bson/-/bson-4.6.1.tgz", 130 | "integrity": "sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw==", 131 | "dependencies": { 132 | "buffer": "^5.6.0" 133 | }, 134 | "engines": { 135 | "node": ">=6.9.0" 136 | } 137 | }, 138 | "node_modules/buffer": { 139 | "version": "5.7.1", 140 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 141 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 142 | "funding": [ 143 | { 144 | "type": "github", 145 | "url": "https://github.com/sponsors/feross" 146 | }, 147 | { 148 | "type": "patreon", 149 | "url": "https://www.patreon.com/feross" 150 | }, 151 | { 152 | "type": "consulting", 153 | "url": "https://feross.org/support" 154 | } 155 | ], 156 | "dependencies": { 157 | "base64-js": "^1.3.1", 158 | "ieee754": "^1.1.13" 159 | } 160 | }, 161 | "node_modules/buffer-equal-constant-time": { 162 | "version": "1.0.1", 163 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 164 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 165 | }, 166 | "node_modules/bytes": { 167 | "version": "3.1.2", 168 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 169 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 170 | "engines": { 171 | "node": ">= 0.8" 172 | } 173 | }, 174 | "node_modules/chalk": { 175 | "version": "4.1.2", 176 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 177 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 178 | "dependencies": { 179 | "ansi-styles": "^4.1.0", 180 | "supports-color": "^7.1.0" 181 | }, 182 | "engines": { 183 | "node": ">=10" 184 | }, 185 | "funding": { 186 | "url": "https://github.com/chalk/chalk?sponsor=1" 187 | } 188 | }, 189 | "node_modules/chalk/node_modules/supports-color": { 190 | "version": "7.2.0", 191 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 192 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 193 | "dependencies": { 194 | "has-flag": "^4.0.0" 195 | }, 196 | "engines": { 197 | "node": ">=8" 198 | } 199 | }, 200 | "node_modules/cliui": { 201 | "version": "7.0.4", 202 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 203 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 204 | "dependencies": { 205 | "string-width": "^4.2.0", 206 | "strip-ansi": "^6.0.0", 207 | "wrap-ansi": "^7.0.0" 208 | } 209 | }, 210 | "node_modules/color-convert": { 211 | "version": "2.0.1", 212 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 213 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 214 | "dependencies": { 215 | "color-name": "~1.1.4" 216 | }, 217 | "engines": { 218 | "node": ">=7.0.0" 219 | } 220 | }, 221 | "node_modules/color-name": { 222 | "version": "1.1.4", 223 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 224 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 225 | }, 226 | "node_modules/colors": { 227 | "version": "1.4.0", 228 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 229 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", 230 | "engines": { 231 | "node": ">=0.1.90" 232 | } 233 | }, 234 | "node_modules/concurrently": { 235 | "version": "7.0.0", 236 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.0.0.tgz", 237 | "integrity": "sha512-WKM7PUsI8wyXpF80H+zjHP32fsgsHNQfPLw/e70Z5dYkV7hF+rf8q3D+ScWJIEr57CpkO3OWBko6hwhQLPR8Pw==", 238 | "dependencies": { 239 | "chalk": "^4.1.0", 240 | "date-fns": "^2.16.1", 241 | "lodash": "^4.17.21", 242 | "rxjs": "^6.6.3", 243 | "spawn-command": "^0.0.2-1", 244 | "supports-color": "^8.1.0", 245 | "tree-kill": "^1.2.2", 246 | "yargs": "^16.2.0" 247 | }, 248 | "bin": { 249 | "concurrently": "dist/bin/concurrently.js" 250 | }, 251 | "engines": { 252 | "node": "^12.20.0 || ^14.13.0 || >=16.0.0" 253 | } 254 | }, 255 | "node_modules/content-disposition": { 256 | "version": "0.5.4", 257 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 258 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 259 | "dependencies": { 260 | "safe-buffer": "5.2.1" 261 | }, 262 | "engines": { 263 | "node": ">= 0.6" 264 | } 265 | }, 266 | "node_modules/content-type": { 267 | "version": "1.0.4", 268 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 269 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", 270 | "engines": { 271 | "node": ">= 0.6" 272 | } 273 | }, 274 | "node_modules/cookie": { 275 | "version": "0.4.2", 276 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", 277 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", 278 | "engines": { 279 | "node": ">= 0.6" 280 | } 281 | }, 282 | "node_modules/cookie-signature": { 283 | "version": "1.0.6", 284 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 285 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 286 | }, 287 | "node_modules/date-fns": { 288 | "version": "2.28.0", 289 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", 290 | "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==", 291 | "engines": { 292 | "node": ">=0.11" 293 | }, 294 | "funding": { 295 | "type": "opencollective", 296 | "url": "https://opencollective.com/date-fns" 297 | } 298 | }, 299 | "node_modules/debug": { 300 | "version": "2.6.9", 301 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 302 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 303 | "dependencies": { 304 | "ms": "2.0.0" 305 | } 306 | }, 307 | "node_modules/denque": { 308 | "version": "2.0.1", 309 | "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", 310 | "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==", 311 | "engines": { 312 | "node": ">=0.10" 313 | } 314 | }, 315 | "node_modules/depd": { 316 | "version": "1.1.2", 317 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 318 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", 319 | "engines": { 320 | "node": ">= 0.6" 321 | } 322 | }, 323 | "node_modules/destroy": { 324 | "version": "1.0.4", 325 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 326 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 327 | }, 328 | "node_modules/dotenv": { 329 | "version": "16.0.0", 330 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", 331 | "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", 332 | "engines": { 333 | "node": ">=12" 334 | } 335 | }, 336 | "node_modules/ecdsa-sig-formatter": { 337 | "version": "1.0.11", 338 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 339 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 340 | "dependencies": { 341 | "safe-buffer": "^5.0.1" 342 | } 343 | }, 344 | "node_modules/ee-first": { 345 | "version": "1.1.1", 346 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 347 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 348 | }, 349 | "node_modules/emoji-regex": { 350 | "version": "8.0.0", 351 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 352 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 353 | }, 354 | "node_modules/encodeurl": { 355 | "version": "1.0.2", 356 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 357 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", 358 | "engines": { 359 | "node": ">= 0.8" 360 | } 361 | }, 362 | "node_modules/escalade": { 363 | "version": "3.1.1", 364 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 365 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 366 | "engines": { 367 | "node": ">=6" 368 | } 369 | }, 370 | "node_modules/escape-html": { 371 | "version": "1.0.3", 372 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 373 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 374 | }, 375 | "node_modules/etag": { 376 | "version": "1.8.1", 377 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 378 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", 379 | "engines": { 380 | "node": ">= 0.6" 381 | } 382 | }, 383 | "node_modules/express": { 384 | "version": "4.17.3", 385 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", 386 | "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", 387 | "dependencies": { 388 | "accepts": "~1.3.8", 389 | "array-flatten": "1.1.1", 390 | "body-parser": "1.19.2", 391 | "content-disposition": "0.5.4", 392 | "content-type": "~1.0.4", 393 | "cookie": "0.4.2", 394 | "cookie-signature": "1.0.6", 395 | "debug": "2.6.9", 396 | "depd": "~1.1.2", 397 | "encodeurl": "~1.0.2", 398 | "escape-html": "~1.0.3", 399 | "etag": "~1.8.1", 400 | "finalhandler": "~1.1.2", 401 | "fresh": "0.5.2", 402 | "merge-descriptors": "1.0.1", 403 | "methods": "~1.1.2", 404 | "on-finished": "~2.3.0", 405 | "parseurl": "~1.3.3", 406 | "path-to-regexp": "0.1.7", 407 | "proxy-addr": "~2.0.7", 408 | "qs": "6.9.7", 409 | "range-parser": "~1.2.1", 410 | "safe-buffer": "5.2.1", 411 | "send": "0.17.2", 412 | "serve-static": "1.14.2", 413 | "setprototypeof": "1.2.0", 414 | "statuses": "~1.5.0", 415 | "type-is": "~1.6.18", 416 | "utils-merge": "1.0.1", 417 | "vary": "~1.1.2" 418 | }, 419 | "engines": { 420 | "node": ">= 0.10.0" 421 | } 422 | }, 423 | "node_modules/express-async-handler": { 424 | "version": "1.2.0", 425 | "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", 426 | "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==", 427 | "dev": true 428 | }, 429 | "node_modules/finalhandler": { 430 | "version": "1.1.2", 431 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 432 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 433 | "dependencies": { 434 | "debug": "2.6.9", 435 | "encodeurl": "~1.0.2", 436 | "escape-html": "~1.0.3", 437 | "on-finished": "~2.3.0", 438 | "parseurl": "~1.3.3", 439 | "statuses": "~1.5.0", 440 | "unpipe": "~1.0.0" 441 | }, 442 | "engines": { 443 | "node": ">= 0.8" 444 | } 445 | }, 446 | "node_modules/forwarded": { 447 | "version": "0.2.0", 448 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 449 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 450 | "engines": { 451 | "node": ">= 0.6" 452 | } 453 | }, 454 | "node_modules/fresh": { 455 | "version": "0.5.2", 456 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 457 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", 458 | "engines": { 459 | "node": ">= 0.6" 460 | } 461 | }, 462 | "node_modules/get-caller-file": { 463 | "version": "2.0.5", 464 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 465 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 466 | "engines": { 467 | "node": "6.* || 8.* || >= 10.*" 468 | } 469 | }, 470 | "node_modules/has-flag": { 471 | "version": "4.0.0", 472 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 473 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 474 | "engines": { 475 | "node": ">=8" 476 | } 477 | }, 478 | "node_modules/http-errors": { 479 | "version": "1.8.1", 480 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", 481 | "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", 482 | "dependencies": { 483 | "depd": "~1.1.2", 484 | "inherits": "2.0.4", 485 | "setprototypeof": "1.2.0", 486 | "statuses": ">= 1.5.0 < 2", 487 | "toidentifier": "1.0.1" 488 | }, 489 | "engines": { 490 | "node": ">= 0.6" 491 | } 492 | }, 493 | "node_modules/iconv-lite": { 494 | "version": "0.4.24", 495 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 496 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 497 | "dependencies": { 498 | "safer-buffer": ">= 2.1.2 < 3" 499 | }, 500 | "engines": { 501 | "node": ">=0.10.0" 502 | } 503 | }, 504 | "node_modules/ieee754": { 505 | "version": "1.2.1", 506 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 507 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 508 | "funding": [ 509 | { 510 | "type": "github", 511 | "url": "https://github.com/sponsors/feross" 512 | }, 513 | { 514 | "type": "patreon", 515 | "url": "https://www.patreon.com/feross" 516 | }, 517 | { 518 | "type": "consulting", 519 | "url": "https://feross.org/support" 520 | } 521 | ] 522 | }, 523 | "node_modules/inherits": { 524 | "version": "2.0.4", 525 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 526 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 527 | }, 528 | "node_modules/ip": { 529 | "version": "1.1.5", 530 | "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", 531 | "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" 532 | }, 533 | "node_modules/ipaddr.js": { 534 | "version": "1.9.1", 535 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 536 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 537 | "engines": { 538 | "node": ">= 0.10" 539 | } 540 | }, 541 | "node_modules/is-fullwidth-code-point": { 542 | "version": "3.0.0", 543 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 544 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 545 | "engines": { 546 | "node": ">=8" 547 | } 548 | }, 549 | "node_modules/jsonwebtoken": { 550 | "version": "8.5.1", 551 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 552 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 553 | "dependencies": { 554 | "jws": "^3.2.2", 555 | "lodash.includes": "^4.3.0", 556 | "lodash.isboolean": "^3.0.3", 557 | "lodash.isinteger": "^4.0.4", 558 | "lodash.isnumber": "^3.0.3", 559 | "lodash.isplainobject": "^4.0.6", 560 | "lodash.isstring": "^4.0.1", 561 | "lodash.once": "^4.0.0", 562 | "ms": "^2.1.1", 563 | "semver": "^5.6.0" 564 | }, 565 | "engines": { 566 | "node": ">=4", 567 | "npm": ">=1.4.28" 568 | } 569 | }, 570 | "node_modules/jsonwebtoken/node_modules/ms": { 571 | "version": "2.1.3", 572 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 573 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 574 | }, 575 | "node_modules/jwa": { 576 | "version": "1.4.1", 577 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 578 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 579 | "dependencies": { 580 | "buffer-equal-constant-time": "1.0.1", 581 | "ecdsa-sig-formatter": "1.0.11", 582 | "safe-buffer": "^5.0.1" 583 | } 584 | }, 585 | "node_modules/jws": { 586 | "version": "3.2.2", 587 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 588 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 589 | "dependencies": { 590 | "jwa": "^1.4.1", 591 | "safe-buffer": "^5.0.1" 592 | } 593 | }, 594 | "node_modules/kareem": { 595 | "version": "2.3.4", 596 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.4.tgz", 597 | "integrity": "sha512-Vcrt8lcpVl0s8ePx634BxwRqmFo+5DcOhlmNadehxreMTIQi/9hOL/B3hZQQbK5DgMS7Lem3xABXV7/S3jy+7g==" 598 | }, 599 | "node_modules/lodash": { 600 | "version": "4.17.21", 601 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 602 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 603 | }, 604 | "node_modules/lodash.includes": { 605 | "version": "4.3.0", 606 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 607 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 608 | }, 609 | "node_modules/lodash.isboolean": { 610 | "version": "3.0.3", 611 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 612 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 613 | }, 614 | "node_modules/lodash.isinteger": { 615 | "version": "4.0.4", 616 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 617 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 618 | }, 619 | "node_modules/lodash.isnumber": { 620 | "version": "3.0.3", 621 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 622 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 623 | }, 624 | "node_modules/lodash.isplainobject": { 625 | "version": "4.0.6", 626 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 627 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 628 | }, 629 | "node_modules/lodash.isstring": { 630 | "version": "4.0.1", 631 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 632 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 633 | }, 634 | "node_modules/lodash.once": { 635 | "version": "4.1.1", 636 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 637 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 638 | }, 639 | "node_modules/media-typer": { 640 | "version": "0.3.0", 641 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 642 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", 643 | "engines": { 644 | "node": ">= 0.6" 645 | } 646 | }, 647 | "node_modules/memory-pager": { 648 | "version": "1.5.0", 649 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 650 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 651 | "optional": true 652 | }, 653 | "node_modules/merge-descriptors": { 654 | "version": "1.0.1", 655 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 656 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 657 | }, 658 | "node_modules/methods": { 659 | "version": "1.1.2", 660 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 661 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", 662 | "engines": { 663 | "node": ">= 0.6" 664 | } 665 | }, 666 | "node_modules/mime": { 667 | "version": "1.6.0", 668 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 669 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 670 | "bin": { 671 | "mime": "cli.js" 672 | }, 673 | "engines": { 674 | "node": ">=4" 675 | } 676 | }, 677 | "node_modules/mime-db": { 678 | "version": "1.51.0", 679 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", 680 | "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", 681 | "engines": { 682 | "node": ">= 0.6" 683 | } 684 | }, 685 | "node_modules/mime-types": { 686 | "version": "2.1.34", 687 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", 688 | "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", 689 | "dependencies": { 690 | "mime-db": "1.51.0" 691 | }, 692 | "engines": { 693 | "node": ">= 0.6" 694 | } 695 | }, 696 | "node_modules/mongodb": { 697 | "version": "4.3.1", 698 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.3.1.tgz", 699 | "integrity": "sha512-sNa8APSIk+r4x31ZwctKjuPSaeKuvUeNb/fu/3B6dRM02HpEgig7hTHM8A/PJQTlxuC/KFWlDlQjhsk/S43tBg==", 700 | "dependencies": { 701 | "bson": "^4.6.1", 702 | "denque": "^2.0.1", 703 | "mongodb-connection-string-url": "^2.4.1", 704 | "socks": "^2.6.1" 705 | }, 706 | "engines": { 707 | "node": ">=12.9.0" 708 | }, 709 | "optionalDependencies": { 710 | "saslprep": "^1.0.3" 711 | } 712 | }, 713 | "node_modules/mongodb-connection-string-url": { 714 | "version": "2.5.2", 715 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.2.tgz", 716 | "integrity": "sha512-tWDyIG8cQlI5k3skB6ywaEA5F9f5OntrKKsT/Lteub2zgwSUlhqEN2inGgBTm8bpYJf8QYBdA/5naz65XDpczA==", 717 | "dependencies": { 718 | "@types/whatwg-url": "^8.2.1", 719 | "whatwg-url": "^11.0.0" 720 | } 721 | }, 722 | "node_modules/mongoose": { 723 | "version": "6.2.3", 724 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.2.3.tgz", 725 | "integrity": "sha512-FxF2D0MGGIw9bAJ57nSyM4Hs4tDHbu6dn9gQwT1J/lxmRB8jfaWWJ3FSJXTmeYlQ6BpyKeIaT8fj6SAX0YMNBA==", 726 | "dependencies": { 727 | "bson": "^4.2.2", 728 | "kareem": "2.3.4", 729 | "mongodb": "4.3.1", 730 | "mpath": "0.8.4", 731 | "mquery": "4.0.2", 732 | "ms": "2.1.3", 733 | "sift": "16.0.0" 734 | }, 735 | "engines": { 736 | "node": ">=12.0.0" 737 | }, 738 | "funding": { 739 | "type": "opencollective", 740 | "url": "https://opencollective.com/mongoose" 741 | } 742 | }, 743 | "node_modules/mongoose/node_modules/ms": { 744 | "version": "2.1.3", 745 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 746 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 747 | }, 748 | "node_modules/mpath": { 749 | "version": "0.8.4", 750 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", 751 | "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==", 752 | "engines": { 753 | "node": ">=4.0.0" 754 | } 755 | }, 756 | "node_modules/mquery": { 757 | "version": "4.0.2", 758 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.2.tgz", 759 | "integrity": "sha512-oAVF0Nil1mT3rxty6Zln4YiD6x6QsUWYz927jZzjMxOK2aqmhEz5JQ7xmrKK7xRFA2dwV+YaOpKU/S+vfNqKxA==", 760 | "dependencies": { 761 | "debug": "4.x" 762 | }, 763 | "engines": { 764 | "node": ">=12.0.0" 765 | } 766 | }, 767 | "node_modules/mquery/node_modules/debug": { 768 | "version": "4.3.3", 769 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 770 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 771 | "dependencies": { 772 | "ms": "2.1.2" 773 | }, 774 | "engines": { 775 | "node": ">=6.0" 776 | }, 777 | "peerDependenciesMeta": { 778 | "supports-color": { 779 | "optional": true 780 | } 781 | } 782 | }, 783 | "node_modules/mquery/node_modules/ms": { 784 | "version": "2.1.2", 785 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 786 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 787 | }, 788 | "node_modules/ms": { 789 | "version": "2.0.0", 790 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 791 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 792 | }, 793 | "node_modules/negotiator": { 794 | "version": "0.6.3", 795 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 796 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 797 | "engines": { 798 | "node": ">= 0.6" 799 | } 800 | }, 801 | "node_modules/nodeman": { 802 | "version": "1.1.2", 803 | "resolved": "https://registry.npmjs.org/nodeman/-/nodeman-1.1.2.tgz", 804 | "integrity": "sha1-eBKFCPdUDbv/v38QkyIuCdWOG2Q=", 805 | "dev": true, 806 | "dependencies": { 807 | "colors": "*" 808 | }, 809 | "bin": { 810 | "_nodeman": "bin/_nodeman", 811 | "nodeman": "bin/nodeman" 812 | }, 813 | "engines": { 814 | "node": "*" 815 | } 816 | }, 817 | "node_modules/on-finished": { 818 | "version": "2.3.0", 819 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 820 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 821 | "dependencies": { 822 | "ee-first": "1.1.1" 823 | }, 824 | "engines": { 825 | "node": ">= 0.8" 826 | } 827 | }, 828 | "node_modules/parseurl": { 829 | "version": "1.3.3", 830 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 831 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 832 | "engines": { 833 | "node": ">= 0.8" 834 | } 835 | }, 836 | "node_modules/path-to-regexp": { 837 | "version": "0.1.7", 838 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 839 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 840 | }, 841 | "node_modules/proxy-addr": { 842 | "version": "2.0.7", 843 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 844 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 845 | "dependencies": { 846 | "forwarded": "0.2.0", 847 | "ipaddr.js": "1.9.1" 848 | }, 849 | "engines": { 850 | "node": ">= 0.10" 851 | } 852 | }, 853 | "node_modules/punycode": { 854 | "version": "2.1.1", 855 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 856 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 857 | "engines": { 858 | "node": ">=6" 859 | } 860 | }, 861 | "node_modules/qs": { 862 | "version": "6.9.7", 863 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", 864 | "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", 865 | "engines": { 866 | "node": ">=0.6" 867 | }, 868 | "funding": { 869 | "url": "https://github.com/sponsors/ljharb" 870 | } 871 | }, 872 | "node_modules/range-parser": { 873 | "version": "1.2.1", 874 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 875 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 876 | "engines": { 877 | "node": ">= 0.6" 878 | } 879 | }, 880 | "node_modules/raw-body": { 881 | "version": "2.4.3", 882 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", 883 | "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", 884 | "dependencies": { 885 | "bytes": "3.1.2", 886 | "http-errors": "1.8.1", 887 | "iconv-lite": "0.4.24", 888 | "unpipe": "1.0.0" 889 | }, 890 | "engines": { 891 | "node": ">= 0.8" 892 | } 893 | }, 894 | "node_modules/require-directory": { 895 | "version": "2.1.1", 896 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 897 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 898 | "engines": { 899 | "node": ">=0.10.0" 900 | } 901 | }, 902 | "node_modules/rxjs": { 903 | "version": "6.6.7", 904 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", 905 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", 906 | "dependencies": { 907 | "tslib": "^1.9.0" 908 | }, 909 | "engines": { 910 | "npm": ">=2.0.0" 911 | } 912 | }, 913 | "node_modules/safe-buffer": { 914 | "version": "5.2.1", 915 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 916 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 917 | "funding": [ 918 | { 919 | "type": "github", 920 | "url": "https://github.com/sponsors/feross" 921 | }, 922 | { 923 | "type": "patreon", 924 | "url": "https://www.patreon.com/feross" 925 | }, 926 | { 927 | "type": "consulting", 928 | "url": "https://feross.org/support" 929 | } 930 | ] 931 | }, 932 | "node_modules/safer-buffer": { 933 | "version": "2.1.2", 934 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 935 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 936 | }, 937 | "node_modules/saslprep": { 938 | "version": "1.0.3", 939 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", 940 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", 941 | "optional": true, 942 | "dependencies": { 943 | "sparse-bitfield": "^3.0.3" 944 | }, 945 | "engines": { 946 | "node": ">=6" 947 | } 948 | }, 949 | "node_modules/semver": { 950 | "version": "5.7.1", 951 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 952 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 953 | "bin": { 954 | "semver": "bin/semver" 955 | } 956 | }, 957 | "node_modules/send": { 958 | "version": "0.17.2", 959 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", 960 | "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", 961 | "dependencies": { 962 | "debug": "2.6.9", 963 | "depd": "~1.1.2", 964 | "destroy": "~1.0.4", 965 | "encodeurl": "~1.0.2", 966 | "escape-html": "~1.0.3", 967 | "etag": "~1.8.1", 968 | "fresh": "0.5.2", 969 | "http-errors": "1.8.1", 970 | "mime": "1.6.0", 971 | "ms": "2.1.3", 972 | "on-finished": "~2.3.0", 973 | "range-parser": "~1.2.1", 974 | "statuses": "~1.5.0" 975 | }, 976 | "engines": { 977 | "node": ">= 0.8.0" 978 | } 979 | }, 980 | "node_modules/send/node_modules/ms": { 981 | "version": "2.1.3", 982 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 983 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 984 | }, 985 | "node_modules/serve-static": { 986 | "version": "1.14.2", 987 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", 988 | "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", 989 | "dependencies": { 990 | "encodeurl": "~1.0.2", 991 | "escape-html": "~1.0.3", 992 | "parseurl": "~1.3.3", 993 | "send": "0.17.2" 994 | }, 995 | "engines": { 996 | "node": ">= 0.8.0" 997 | } 998 | }, 999 | "node_modules/setprototypeof": { 1000 | "version": "1.2.0", 1001 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1002 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1003 | }, 1004 | "node_modules/sift": { 1005 | "version": "16.0.0", 1006 | "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz", 1007 | "integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ==" 1008 | }, 1009 | "node_modules/smart-buffer": { 1010 | "version": "4.2.0", 1011 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", 1012 | "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", 1013 | "engines": { 1014 | "node": ">= 6.0.0", 1015 | "npm": ">= 3.0.0" 1016 | } 1017 | }, 1018 | "node_modules/socks": { 1019 | "version": "2.6.2", 1020 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", 1021 | "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", 1022 | "dependencies": { 1023 | "ip": "^1.1.5", 1024 | "smart-buffer": "^4.2.0" 1025 | }, 1026 | "engines": { 1027 | "node": ">= 10.13.0", 1028 | "npm": ">= 3.0.0" 1029 | } 1030 | }, 1031 | "node_modules/sparse-bitfield": { 1032 | "version": "3.0.3", 1033 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 1034 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 1035 | "optional": true, 1036 | "dependencies": { 1037 | "memory-pager": "^1.0.2" 1038 | } 1039 | }, 1040 | "node_modules/spawn-command": { 1041 | "version": "0.0.2-1", 1042 | "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", 1043 | "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" 1044 | }, 1045 | "node_modules/statuses": { 1046 | "version": "1.5.0", 1047 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1048 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", 1049 | "engines": { 1050 | "node": ">= 0.6" 1051 | } 1052 | }, 1053 | "node_modules/string-width": { 1054 | "version": "4.2.3", 1055 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1056 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1057 | "dependencies": { 1058 | "emoji-regex": "^8.0.0", 1059 | "is-fullwidth-code-point": "^3.0.0", 1060 | "strip-ansi": "^6.0.1" 1061 | }, 1062 | "engines": { 1063 | "node": ">=8" 1064 | } 1065 | }, 1066 | "node_modules/strip-ansi": { 1067 | "version": "6.0.1", 1068 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1069 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1070 | "dependencies": { 1071 | "ansi-regex": "^5.0.1" 1072 | }, 1073 | "engines": { 1074 | "node": ">=8" 1075 | } 1076 | }, 1077 | "node_modules/supports-color": { 1078 | "version": "8.1.1", 1079 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1080 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1081 | "dependencies": { 1082 | "has-flag": "^4.0.0" 1083 | }, 1084 | "engines": { 1085 | "node": ">=10" 1086 | }, 1087 | "funding": { 1088 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1089 | } 1090 | }, 1091 | "node_modules/toidentifier": { 1092 | "version": "1.0.1", 1093 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1094 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1095 | "engines": { 1096 | "node": ">=0.6" 1097 | } 1098 | }, 1099 | "node_modules/tr46": { 1100 | "version": "3.0.0", 1101 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", 1102 | "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", 1103 | "dependencies": { 1104 | "punycode": "^2.1.1" 1105 | }, 1106 | "engines": { 1107 | "node": ">=12" 1108 | } 1109 | }, 1110 | "node_modules/tree-kill": { 1111 | "version": "1.2.2", 1112 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 1113 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 1114 | "bin": { 1115 | "tree-kill": "cli.js" 1116 | } 1117 | }, 1118 | "node_modules/tslib": { 1119 | "version": "1.14.1", 1120 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 1121 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" 1122 | }, 1123 | "node_modules/type-is": { 1124 | "version": "1.6.18", 1125 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1126 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1127 | "dependencies": { 1128 | "media-typer": "0.3.0", 1129 | "mime-types": "~2.1.24" 1130 | }, 1131 | "engines": { 1132 | "node": ">= 0.6" 1133 | } 1134 | }, 1135 | "node_modules/unpipe": { 1136 | "version": "1.0.0", 1137 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1138 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", 1139 | "engines": { 1140 | "node": ">= 0.8" 1141 | } 1142 | }, 1143 | "node_modules/utils-merge": { 1144 | "version": "1.0.1", 1145 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1146 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", 1147 | "engines": { 1148 | "node": ">= 0.4.0" 1149 | } 1150 | }, 1151 | "node_modules/vary": { 1152 | "version": "1.1.2", 1153 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1154 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", 1155 | "engines": { 1156 | "node": ">= 0.8" 1157 | } 1158 | }, 1159 | "node_modules/webidl-conversions": { 1160 | "version": "7.0.0", 1161 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 1162 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 1163 | "engines": { 1164 | "node": ">=12" 1165 | } 1166 | }, 1167 | "node_modules/whatwg-url": { 1168 | "version": "11.0.0", 1169 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", 1170 | "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", 1171 | "dependencies": { 1172 | "tr46": "^3.0.0", 1173 | "webidl-conversions": "^7.0.0" 1174 | }, 1175 | "engines": { 1176 | "node": ">=12" 1177 | } 1178 | }, 1179 | "node_modules/wrap-ansi": { 1180 | "version": "7.0.0", 1181 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1182 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1183 | "dependencies": { 1184 | "ansi-styles": "^4.0.0", 1185 | "string-width": "^4.1.0", 1186 | "strip-ansi": "^6.0.0" 1187 | }, 1188 | "engines": { 1189 | "node": ">=10" 1190 | }, 1191 | "funding": { 1192 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1193 | } 1194 | }, 1195 | "node_modules/y18n": { 1196 | "version": "5.0.8", 1197 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1198 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1199 | "engines": { 1200 | "node": ">=10" 1201 | } 1202 | }, 1203 | "node_modules/yargs": { 1204 | "version": "16.2.0", 1205 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1206 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1207 | "dependencies": { 1208 | "cliui": "^7.0.2", 1209 | "escalade": "^3.1.1", 1210 | "get-caller-file": "^2.0.5", 1211 | "require-directory": "^2.1.1", 1212 | "string-width": "^4.2.0", 1213 | "y18n": "^5.0.5", 1214 | "yargs-parser": "^20.2.2" 1215 | }, 1216 | "engines": { 1217 | "node": ">=10" 1218 | } 1219 | }, 1220 | "node_modules/yargs-parser": { 1221 | "version": "20.2.9", 1222 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 1223 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", 1224 | "engines": { 1225 | "node": ">=10" 1226 | } 1227 | } 1228 | }, 1229 | "dependencies": { 1230 | "@types/node": { 1231 | "version": "17.0.21", 1232 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", 1233 | "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==" 1234 | }, 1235 | "@types/webidl-conversions": { 1236 | "version": "6.1.1", 1237 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz", 1238 | "integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q==" 1239 | }, 1240 | "@types/whatwg-url": { 1241 | "version": "8.2.1", 1242 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz", 1243 | "integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==", 1244 | "requires": { 1245 | "@types/node": "*", 1246 | "@types/webidl-conversions": "*" 1247 | } 1248 | }, 1249 | "accepts": { 1250 | "version": "1.3.8", 1251 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 1252 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 1253 | "requires": { 1254 | "mime-types": "~2.1.34", 1255 | "negotiator": "0.6.3" 1256 | } 1257 | }, 1258 | "ansi-regex": { 1259 | "version": "5.0.1", 1260 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1261 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 1262 | }, 1263 | "ansi-styles": { 1264 | "version": "4.3.0", 1265 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1266 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1267 | "requires": { 1268 | "color-convert": "^2.0.1" 1269 | } 1270 | }, 1271 | "array-flatten": { 1272 | "version": "1.1.1", 1273 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 1274 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 1275 | }, 1276 | "base64-js": { 1277 | "version": "1.5.1", 1278 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 1279 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 1280 | }, 1281 | "bcryptjs": { 1282 | "version": "2.4.3", 1283 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", 1284 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 1285 | }, 1286 | "body-parser": { 1287 | "version": "1.19.2", 1288 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", 1289 | "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", 1290 | "requires": { 1291 | "bytes": "3.1.2", 1292 | "content-type": "~1.0.4", 1293 | "debug": "2.6.9", 1294 | "depd": "~1.1.2", 1295 | "http-errors": "1.8.1", 1296 | "iconv-lite": "0.4.24", 1297 | "on-finished": "~2.3.0", 1298 | "qs": "6.9.7", 1299 | "raw-body": "2.4.3", 1300 | "type-is": "~1.6.18" 1301 | } 1302 | }, 1303 | "bson": { 1304 | "version": "4.6.1", 1305 | "resolved": "https://registry.npmjs.org/bson/-/bson-4.6.1.tgz", 1306 | "integrity": "sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw==", 1307 | "requires": { 1308 | "buffer": "^5.6.0" 1309 | } 1310 | }, 1311 | "buffer": { 1312 | "version": "5.7.1", 1313 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 1314 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 1315 | "requires": { 1316 | "base64-js": "^1.3.1", 1317 | "ieee754": "^1.1.13" 1318 | } 1319 | }, 1320 | "buffer-equal-constant-time": { 1321 | "version": "1.0.1", 1322 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 1323 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 1324 | }, 1325 | "bytes": { 1326 | "version": "3.1.2", 1327 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 1328 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 1329 | }, 1330 | "chalk": { 1331 | "version": "4.1.2", 1332 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1333 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1334 | "requires": { 1335 | "ansi-styles": "^4.1.0", 1336 | "supports-color": "^7.1.0" 1337 | }, 1338 | "dependencies": { 1339 | "supports-color": { 1340 | "version": "7.2.0", 1341 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1342 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1343 | "requires": { 1344 | "has-flag": "^4.0.0" 1345 | } 1346 | } 1347 | } 1348 | }, 1349 | "cliui": { 1350 | "version": "7.0.4", 1351 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 1352 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 1353 | "requires": { 1354 | "string-width": "^4.2.0", 1355 | "strip-ansi": "^6.0.0", 1356 | "wrap-ansi": "^7.0.0" 1357 | } 1358 | }, 1359 | "color-convert": { 1360 | "version": "2.0.1", 1361 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1362 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1363 | "requires": { 1364 | "color-name": "~1.1.4" 1365 | } 1366 | }, 1367 | "color-name": { 1368 | "version": "1.1.4", 1369 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1370 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 1371 | }, 1372 | "colors": { 1373 | "version": "1.4.0", 1374 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 1375 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" 1376 | }, 1377 | "concurrently": { 1378 | "version": "7.0.0", 1379 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.0.0.tgz", 1380 | "integrity": "sha512-WKM7PUsI8wyXpF80H+zjHP32fsgsHNQfPLw/e70Z5dYkV7hF+rf8q3D+ScWJIEr57CpkO3OWBko6hwhQLPR8Pw==", 1381 | "requires": { 1382 | "chalk": "^4.1.0", 1383 | "date-fns": "^2.16.1", 1384 | "lodash": "^4.17.21", 1385 | "rxjs": "^6.6.3", 1386 | "spawn-command": "^0.0.2-1", 1387 | "supports-color": "^8.1.0", 1388 | "tree-kill": "^1.2.2", 1389 | "yargs": "^16.2.0" 1390 | } 1391 | }, 1392 | "content-disposition": { 1393 | "version": "0.5.4", 1394 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 1395 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 1396 | "requires": { 1397 | "safe-buffer": "5.2.1" 1398 | } 1399 | }, 1400 | "content-type": { 1401 | "version": "1.0.4", 1402 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 1403 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 1404 | }, 1405 | "cookie": { 1406 | "version": "0.4.2", 1407 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", 1408 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" 1409 | }, 1410 | "cookie-signature": { 1411 | "version": "1.0.6", 1412 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 1413 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 1414 | }, 1415 | "date-fns": { 1416 | "version": "2.28.0", 1417 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", 1418 | "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" 1419 | }, 1420 | "debug": { 1421 | "version": "2.6.9", 1422 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1423 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1424 | "requires": { 1425 | "ms": "2.0.0" 1426 | } 1427 | }, 1428 | "denque": { 1429 | "version": "2.0.1", 1430 | "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", 1431 | "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==" 1432 | }, 1433 | "depd": { 1434 | "version": "1.1.2", 1435 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 1436 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 1437 | }, 1438 | "destroy": { 1439 | "version": "1.0.4", 1440 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 1441 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 1442 | }, 1443 | "dotenv": { 1444 | "version": "16.0.0", 1445 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", 1446 | "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==" 1447 | }, 1448 | "ecdsa-sig-formatter": { 1449 | "version": "1.0.11", 1450 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 1451 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 1452 | "requires": { 1453 | "safe-buffer": "^5.0.1" 1454 | } 1455 | }, 1456 | "ee-first": { 1457 | "version": "1.1.1", 1458 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 1459 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 1460 | }, 1461 | "emoji-regex": { 1462 | "version": "8.0.0", 1463 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1464 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 1465 | }, 1466 | "encodeurl": { 1467 | "version": "1.0.2", 1468 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1469 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 1470 | }, 1471 | "escalade": { 1472 | "version": "3.1.1", 1473 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 1474 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" 1475 | }, 1476 | "escape-html": { 1477 | "version": "1.0.3", 1478 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1479 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 1480 | }, 1481 | "etag": { 1482 | "version": "1.8.1", 1483 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 1484 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 1485 | }, 1486 | "express": { 1487 | "version": "4.17.3", 1488 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", 1489 | "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", 1490 | "requires": { 1491 | "accepts": "~1.3.8", 1492 | "array-flatten": "1.1.1", 1493 | "body-parser": "1.19.2", 1494 | "content-disposition": "0.5.4", 1495 | "content-type": "~1.0.4", 1496 | "cookie": "0.4.2", 1497 | "cookie-signature": "1.0.6", 1498 | "debug": "2.6.9", 1499 | "depd": "~1.1.2", 1500 | "encodeurl": "~1.0.2", 1501 | "escape-html": "~1.0.3", 1502 | "etag": "~1.8.1", 1503 | "finalhandler": "~1.1.2", 1504 | "fresh": "0.5.2", 1505 | "merge-descriptors": "1.0.1", 1506 | "methods": "~1.1.2", 1507 | "on-finished": "~2.3.0", 1508 | "parseurl": "~1.3.3", 1509 | "path-to-regexp": "0.1.7", 1510 | "proxy-addr": "~2.0.7", 1511 | "qs": "6.9.7", 1512 | "range-parser": "~1.2.1", 1513 | "safe-buffer": "5.2.1", 1514 | "send": "0.17.2", 1515 | "serve-static": "1.14.2", 1516 | "setprototypeof": "1.2.0", 1517 | "statuses": "~1.5.0", 1518 | "type-is": "~1.6.18", 1519 | "utils-merge": "1.0.1", 1520 | "vary": "~1.1.2" 1521 | } 1522 | }, 1523 | "express-async-handler": { 1524 | "version": "1.2.0", 1525 | "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", 1526 | "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==", 1527 | "dev": true 1528 | }, 1529 | "finalhandler": { 1530 | "version": "1.1.2", 1531 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 1532 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 1533 | "requires": { 1534 | "debug": "2.6.9", 1535 | "encodeurl": "~1.0.2", 1536 | "escape-html": "~1.0.3", 1537 | "on-finished": "~2.3.0", 1538 | "parseurl": "~1.3.3", 1539 | "statuses": "~1.5.0", 1540 | "unpipe": "~1.0.0" 1541 | } 1542 | }, 1543 | "forwarded": { 1544 | "version": "0.2.0", 1545 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 1546 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 1547 | }, 1548 | "fresh": { 1549 | "version": "0.5.2", 1550 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 1551 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 1552 | }, 1553 | "get-caller-file": { 1554 | "version": "2.0.5", 1555 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1556 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 1557 | }, 1558 | "has-flag": { 1559 | "version": "4.0.0", 1560 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1561 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 1562 | }, 1563 | "http-errors": { 1564 | "version": "1.8.1", 1565 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", 1566 | "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", 1567 | "requires": { 1568 | "depd": "~1.1.2", 1569 | "inherits": "2.0.4", 1570 | "setprototypeof": "1.2.0", 1571 | "statuses": ">= 1.5.0 < 2", 1572 | "toidentifier": "1.0.1" 1573 | } 1574 | }, 1575 | "iconv-lite": { 1576 | "version": "0.4.24", 1577 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1578 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1579 | "requires": { 1580 | "safer-buffer": ">= 2.1.2 < 3" 1581 | } 1582 | }, 1583 | "ieee754": { 1584 | "version": "1.2.1", 1585 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1586 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 1587 | }, 1588 | "inherits": { 1589 | "version": "2.0.4", 1590 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1591 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1592 | }, 1593 | "ip": { 1594 | "version": "1.1.5", 1595 | "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", 1596 | "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" 1597 | }, 1598 | "ipaddr.js": { 1599 | "version": "1.9.1", 1600 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1601 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 1602 | }, 1603 | "is-fullwidth-code-point": { 1604 | "version": "3.0.0", 1605 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1606 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 1607 | }, 1608 | "jsonwebtoken": { 1609 | "version": "8.5.1", 1610 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 1611 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 1612 | "requires": { 1613 | "jws": "^3.2.2", 1614 | "lodash.includes": "^4.3.0", 1615 | "lodash.isboolean": "^3.0.3", 1616 | "lodash.isinteger": "^4.0.4", 1617 | "lodash.isnumber": "^3.0.3", 1618 | "lodash.isplainobject": "^4.0.6", 1619 | "lodash.isstring": "^4.0.1", 1620 | "lodash.once": "^4.0.0", 1621 | "ms": "^2.1.1", 1622 | "semver": "^5.6.0" 1623 | }, 1624 | "dependencies": { 1625 | "ms": { 1626 | "version": "2.1.3", 1627 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1628 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1629 | } 1630 | } 1631 | }, 1632 | "jwa": { 1633 | "version": "1.4.1", 1634 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 1635 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 1636 | "requires": { 1637 | "buffer-equal-constant-time": "1.0.1", 1638 | "ecdsa-sig-formatter": "1.0.11", 1639 | "safe-buffer": "^5.0.1" 1640 | } 1641 | }, 1642 | "jws": { 1643 | "version": "3.2.2", 1644 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 1645 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 1646 | "requires": { 1647 | "jwa": "^1.4.1", 1648 | "safe-buffer": "^5.0.1" 1649 | } 1650 | }, 1651 | "kareem": { 1652 | "version": "2.3.4", 1653 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.4.tgz", 1654 | "integrity": "sha512-Vcrt8lcpVl0s8ePx634BxwRqmFo+5DcOhlmNadehxreMTIQi/9hOL/B3hZQQbK5DgMS7Lem3xABXV7/S3jy+7g==" 1655 | }, 1656 | "lodash": { 1657 | "version": "4.17.21", 1658 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 1659 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 1660 | }, 1661 | "lodash.includes": { 1662 | "version": "4.3.0", 1663 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 1664 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 1665 | }, 1666 | "lodash.isboolean": { 1667 | "version": "3.0.3", 1668 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 1669 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 1670 | }, 1671 | "lodash.isinteger": { 1672 | "version": "4.0.4", 1673 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 1674 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 1675 | }, 1676 | "lodash.isnumber": { 1677 | "version": "3.0.3", 1678 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 1679 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 1680 | }, 1681 | "lodash.isplainobject": { 1682 | "version": "4.0.6", 1683 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 1684 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 1685 | }, 1686 | "lodash.isstring": { 1687 | "version": "4.0.1", 1688 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 1689 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 1690 | }, 1691 | "lodash.once": { 1692 | "version": "4.1.1", 1693 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 1694 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 1695 | }, 1696 | "media-typer": { 1697 | "version": "0.3.0", 1698 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1699 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 1700 | }, 1701 | "memory-pager": { 1702 | "version": "1.5.0", 1703 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 1704 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 1705 | "optional": true 1706 | }, 1707 | "merge-descriptors": { 1708 | "version": "1.0.1", 1709 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 1710 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 1711 | }, 1712 | "methods": { 1713 | "version": "1.1.2", 1714 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1715 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 1716 | }, 1717 | "mime": { 1718 | "version": "1.6.0", 1719 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1720 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 1721 | }, 1722 | "mime-db": { 1723 | "version": "1.51.0", 1724 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", 1725 | "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" 1726 | }, 1727 | "mime-types": { 1728 | "version": "2.1.34", 1729 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", 1730 | "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", 1731 | "requires": { 1732 | "mime-db": "1.51.0" 1733 | } 1734 | }, 1735 | "mongodb": { 1736 | "version": "4.3.1", 1737 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.3.1.tgz", 1738 | "integrity": "sha512-sNa8APSIk+r4x31ZwctKjuPSaeKuvUeNb/fu/3B6dRM02HpEgig7hTHM8A/PJQTlxuC/KFWlDlQjhsk/S43tBg==", 1739 | "requires": { 1740 | "bson": "^4.6.1", 1741 | "denque": "^2.0.1", 1742 | "mongodb-connection-string-url": "^2.4.1", 1743 | "saslprep": "^1.0.3", 1744 | "socks": "^2.6.1" 1745 | } 1746 | }, 1747 | "mongodb-connection-string-url": { 1748 | "version": "2.5.2", 1749 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.2.tgz", 1750 | "integrity": "sha512-tWDyIG8cQlI5k3skB6ywaEA5F9f5OntrKKsT/Lteub2zgwSUlhqEN2inGgBTm8bpYJf8QYBdA/5naz65XDpczA==", 1751 | "requires": { 1752 | "@types/whatwg-url": "^8.2.1", 1753 | "whatwg-url": "^11.0.0" 1754 | } 1755 | }, 1756 | "mongoose": { 1757 | "version": "6.2.3", 1758 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.2.3.tgz", 1759 | "integrity": "sha512-FxF2D0MGGIw9bAJ57nSyM4Hs4tDHbu6dn9gQwT1J/lxmRB8jfaWWJ3FSJXTmeYlQ6BpyKeIaT8fj6SAX0YMNBA==", 1760 | "requires": { 1761 | "bson": "^4.2.2", 1762 | "kareem": "2.3.4", 1763 | "mongodb": "4.3.1", 1764 | "mpath": "0.8.4", 1765 | "mquery": "4.0.2", 1766 | "ms": "2.1.3", 1767 | "sift": "16.0.0" 1768 | }, 1769 | "dependencies": { 1770 | "ms": { 1771 | "version": "2.1.3", 1772 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1773 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1774 | } 1775 | } 1776 | }, 1777 | "mpath": { 1778 | "version": "0.8.4", 1779 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", 1780 | "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==" 1781 | }, 1782 | "mquery": { 1783 | "version": "4.0.2", 1784 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.2.tgz", 1785 | "integrity": "sha512-oAVF0Nil1mT3rxty6Zln4YiD6x6QsUWYz927jZzjMxOK2aqmhEz5JQ7xmrKK7xRFA2dwV+YaOpKU/S+vfNqKxA==", 1786 | "requires": { 1787 | "debug": "4.x" 1788 | }, 1789 | "dependencies": { 1790 | "debug": { 1791 | "version": "4.3.3", 1792 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 1793 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 1794 | "requires": { 1795 | "ms": "2.1.2" 1796 | } 1797 | }, 1798 | "ms": { 1799 | "version": "2.1.2", 1800 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1801 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1802 | } 1803 | } 1804 | }, 1805 | "ms": { 1806 | "version": "2.0.0", 1807 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1808 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1809 | }, 1810 | "negotiator": { 1811 | "version": "0.6.3", 1812 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1813 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 1814 | }, 1815 | "nodeman": { 1816 | "version": "1.1.2", 1817 | "resolved": "https://registry.npmjs.org/nodeman/-/nodeman-1.1.2.tgz", 1818 | "integrity": "sha1-eBKFCPdUDbv/v38QkyIuCdWOG2Q=", 1819 | "dev": true, 1820 | "requires": { 1821 | "colors": "*" 1822 | } 1823 | }, 1824 | "on-finished": { 1825 | "version": "2.3.0", 1826 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1827 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1828 | "requires": { 1829 | "ee-first": "1.1.1" 1830 | } 1831 | }, 1832 | "parseurl": { 1833 | "version": "1.3.3", 1834 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1835 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1836 | }, 1837 | "path-to-regexp": { 1838 | "version": "0.1.7", 1839 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1840 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1841 | }, 1842 | "proxy-addr": { 1843 | "version": "2.0.7", 1844 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1845 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1846 | "requires": { 1847 | "forwarded": "0.2.0", 1848 | "ipaddr.js": "1.9.1" 1849 | } 1850 | }, 1851 | "punycode": { 1852 | "version": "2.1.1", 1853 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1854 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 1855 | }, 1856 | "qs": { 1857 | "version": "6.9.7", 1858 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", 1859 | "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" 1860 | }, 1861 | "range-parser": { 1862 | "version": "1.2.1", 1863 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1864 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 1865 | }, 1866 | "raw-body": { 1867 | "version": "2.4.3", 1868 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", 1869 | "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", 1870 | "requires": { 1871 | "bytes": "3.1.2", 1872 | "http-errors": "1.8.1", 1873 | "iconv-lite": "0.4.24", 1874 | "unpipe": "1.0.0" 1875 | } 1876 | }, 1877 | "require-directory": { 1878 | "version": "2.1.1", 1879 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1880 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 1881 | }, 1882 | "rxjs": { 1883 | "version": "6.6.7", 1884 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", 1885 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", 1886 | "requires": { 1887 | "tslib": "^1.9.0" 1888 | } 1889 | }, 1890 | "safe-buffer": { 1891 | "version": "5.2.1", 1892 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1893 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1894 | }, 1895 | "safer-buffer": { 1896 | "version": "2.1.2", 1897 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1898 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1899 | }, 1900 | "saslprep": { 1901 | "version": "1.0.3", 1902 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", 1903 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", 1904 | "optional": true, 1905 | "requires": { 1906 | "sparse-bitfield": "^3.0.3" 1907 | } 1908 | }, 1909 | "semver": { 1910 | "version": "5.7.1", 1911 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1912 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1913 | }, 1914 | "send": { 1915 | "version": "0.17.2", 1916 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", 1917 | "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", 1918 | "requires": { 1919 | "debug": "2.6.9", 1920 | "depd": "~1.1.2", 1921 | "destroy": "~1.0.4", 1922 | "encodeurl": "~1.0.2", 1923 | "escape-html": "~1.0.3", 1924 | "etag": "~1.8.1", 1925 | "fresh": "0.5.2", 1926 | "http-errors": "1.8.1", 1927 | "mime": "1.6.0", 1928 | "ms": "2.1.3", 1929 | "on-finished": "~2.3.0", 1930 | "range-parser": "~1.2.1", 1931 | "statuses": "~1.5.0" 1932 | }, 1933 | "dependencies": { 1934 | "ms": { 1935 | "version": "2.1.3", 1936 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1937 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1938 | } 1939 | } 1940 | }, 1941 | "serve-static": { 1942 | "version": "1.14.2", 1943 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", 1944 | "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", 1945 | "requires": { 1946 | "encodeurl": "~1.0.2", 1947 | "escape-html": "~1.0.3", 1948 | "parseurl": "~1.3.3", 1949 | "send": "0.17.2" 1950 | } 1951 | }, 1952 | "setprototypeof": { 1953 | "version": "1.2.0", 1954 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1955 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1956 | }, 1957 | "sift": { 1958 | "version": "16.0.0", 1959 | "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz", 1960 | "integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ==" 1961 | }, 1962 | "smart-buffer": { 1963 | "version": "4.2.0", 1964 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", 1965 | "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" 1966 | }, 1967 | "socks": { 1968 | "version": "2.6.2", 1969 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", 1970 | "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", 1971 | "requires": { 1972 | "ip": "^1.1.5", 1973 | "smart-buffer": "^4.2.0" 1974 | } 1975 | }, 1976 | "sparse-bitfield": { 1977 | "version": "3.0.3", 1978 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 1979 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 1980 | "optional": true, 1981 | "requires": { 1982 | "memory-pager": "^1.0.2" 1983 | } 1984 | }, 1985 | "spawn-command": { 1986 | "version": "0.0.2-1", 1987 | "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", 1988 | "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" 1989 | }, 1990 | "statuses": { 1991 | "version": "1.5.0", 1992 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1993 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1994 | }, 1995 | "string-width": { 1996 | "version": "4.2.3", 1997 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1998 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1999 | "requires": { 2000 | "emoji-regex": "^8.0.0", 2001 | "is-fullwidth-code-point": "^3.0.0", 2002 | "strip-ansi": "^6.0.1" 2003 | } 2004 | }, 2005 | "strip-ansi": { 2006 | "version": "6.0.1", 2007 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 2008 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 2009 | "requires": { 2010 | "ansi-regex": "^5.0.1" 2011 | } 2012 | }, 2013 | "supports-color": { 2014 | "version": "8.1.1", 2015 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 2016 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 2017 | "requires": { 2018 | "has-flag": "^4.0.0" 2019 | } 2020 | }, 2021 | "toidentifier": { 2022 | "version": "1.0.1", 2023 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 2024 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 2025 | }, 2026 | "tr46": { 2027 | "version": "3.0.0", 2028 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", 2029 | "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", 2030 | "requires": { 2031 | "punycode": "^2.1.1" 2032 | } 2033 | }, 2034 | "tree-kill": { 2035 | "version": "1.2.2", 2036 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 2037 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" 2038 | }, 2039 | "tslib": { 2040 | "version": "1.14.1", 2041 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 2042 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" 2043 | }, 2044 | "type-is": { 2045 | "version": "1.6.18", 2046 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 2047 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 2048 | "requires": { 2049 | "media-typer": "0.3.0", 2050 | "mime-types": "~2.1.24" 2051 | } 2052 | }, 2053 | "unpipe": { 2054 | "version": "1.0.0", 2055 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2056 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 2057 | }, 2058 | "utils-merge": { 2059 | "version": "1.0.1", 2060 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 2061 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 2062 | }, 2063 | "vary": { 2064 | "version": "1.1.2", 2065 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2066 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 2067 | }, 2068 | "webidl-conversions": { 2069 | "version": "7.0.0", 2070 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 2071 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" 2072 | }, 2073 | "whatwg-url": { 2074 | "version": "11.0.0", 2075 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", 2076 | "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", 2077 | "requires": { 2078 | "tr46": "^3.0.0", 2079 | "webidl-conversions": "^7.0.0" 2080 | } 2081 | }, 2082 | "wrap-ansi": { 2083 | "version": "7.0.0", 2084 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 2085 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 2086 | "requires": { 2087 | "ansi-styles": "^4.0.0", 2088 | "string-width": "^4.1.0", 2089 | "strip-ansi": "^6.0.0" 2090 | } 2091 | }, 2092 | "y18n": { 2093 | "version": "5.0.8", 2094 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 2095 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" 2096 | }, 2097 | "yargs": { 2098 | "version": "16.2.0", 2099 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 2100 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 2101 | "requires": { 2102 | "cliui": "^7.0.2", 2103 | "escalade": "^3.1.1", 2104 | "get-caller-file": "^2.0.5", 2105 | "require-directory": "^2.1.1", 2106 | "string-width": "^4.2.0", 2107 | "y18n": "^5.0.5", 2108 | "yargs-parser": "^20.2.2" 2109 | } 2110 | }, 2111 | "yargs-parser": { 2112 | "version": "20.2.9", 2113 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 2114 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" 2115 | } 2116 | } 2117 | } 2118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "support-desk", 3 | "version": "1.0.0", 4 | "description": "Support Ticket App", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node backend/server.js", 8 | "server": "nodemon backend/server.js --ignore client", 9 | "client": "npm start --prefix frontend", 10 | "dev": "concurrently \"npm run server\" \"npm run client\"", 11 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend && npm run build --prefix frontend" 12 | }, 13 | "author": "Mohammad Arif", 14 | "license": "MIT", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "colors": "^1.4.0", 18 | "concurrently": "^7.0.0", 19 | "dotenv": "^16.0.0", 20 | "express": "^4.17.3", 21 | "jsonwebtoken": "^8.5.1", 22 | "express-async-handler": "^1.2.0", 23 | "mongoose": "^6.2.3" 24 | }, 25 | "devDependencies": { 26 | "nodeman": "^1.1.2" 27 | } 28 | } 29 | --------------------------------------------------------------------------------